9 Commits

Author SHA1 Message Date
Hermes Platform Engineer
2f1ae311f7 fix(homeassistant): reduce memory requests to 256Mi, add node affinity for raspberrypi
Node roger-nucbox-evo-x2 is at 3.78% memory free causing home-assistant
pods to flap Pending/Running. Reduce resource requests to fit within
cluster capacity and prefer scheduling on raspberrypi (63% mem free).
2026-07-28 12:58:19 +00:00
7ca919b7c7 Merge pull request 'fix(homeassistant): pin image to 2026.7.4 to avoid cffi version mismatch' (#28) from fix/homeassistant-pin-version into main
Reviewed-on: #28
2026-07-28 11:07:26 +02:00
Hermes Platform Engineer
d266086c1b fix(homeassistant): pin image to 2026.7.4 to avoid cffi version mismatch with Python 3.14 2026-07-28 09:01:06 +00:00
Roger Oriol
d3798f47ac fix homeassistant configuration.yaml not being writable 2026-07-28 00:31:23 +02:00
Roger Oriol
707ec39291 fix hermes api not being reachable 2026-07-26 17:19:20 +02:00
Roger Oriol
5bb1066d0d fix hermes api not being reachable 2026-07-26 17:08:27 +02:00
Roger Oriol
575c7bf9e0 agents fix language and time zone 2026-07-26 14:59:58 +02:00
Roger Oriol
4222582a54 Merge branch 'main' of https://git.rogi.casa/roger/k3s-cluster 2026-07-26 14:38:02 +02:00
Roger Oriol
4f6255800b enable api access for agents home manager and platform engineer 2026-07-26 14:37:37 +02:00
8 changed files with 224 additions and 21 deletions

View File

@@ -174,10 +174,17 @@ data:
run (Hermes disables that). Do not try.
7. **Talk like a concierge.** Short, concrete, with entity_ids and values.
No filler. When you did something, say what in one line.
8. **Respect time of day and presence.** Don't run noisy routines (loud
8. **Language: Catalan by default.** Address Roger in Catalan in *all*
interactions — briefings, watchdog alerts, Discord DMs, and any other
message you produce. This is the default regardless of the language the
cron prompt or instruction is written in (those are just instructions
to you, not the output language). Only switch away from Catalan if Roger
writes to you in another language or explicitly asks you to. Never
default to English or Italian.
9. **Respect time of day and presence.** Don't run noisy routines (loud
media, vacuum robots) at night. Check presence (`zone.*` /
`binary_sensor.*`) and the time before acting.
9. **Absence of presence entities ≠ nobody home.** This Home Assistant
10. **Absence of presence entities ≠ nobody home.** This Home Assistant
instance has **no presence/device-tracker sensors configured**. Do NOT
infer "nobody is home" from the absence of `zone.*`, `device_tracker.*`,
or presence `binary_sensor.*` entities, from `zone.home` being missing /

View File

@@ -13,8 +13,14 @@
# Schedule (4 jobs):
# 1. energy-anomaly — every 4h (silent unless power draw is too high)
# 2. comfort-check — every 4h (silent unless a room is out of range)
# 3. morning-briefing — 0 8 * * * (always delivered)
# 4. evening-briefing — 0 0 * * * (always delivered)
# 3. morning-briefing — 0 6 * * * (always delivered)
# 4. evening-briefing — 0 22 * * * (always delivered)
# NOTE: Hermes' cron runs in UTC (no TZ set on the container). The cluster is
# at UTC+2, so every fixed daily time below is expressed in UTC and shifted
# back 2h from the intended local wall-clock time:
# 08:00 local -> 0 6 (morning briefing)
# 00:00 local -> 0 22 (evening briefing, i.e. midnight local)
# Relative schedules (every 4h) are timezone-independent.
# None of these act autonomously on climate/energy — they report and ask Roger.
---
apiVersion: batch/v1
@@ -87,12 +93,12 @@ spec:
create "comfort-check" "every 4h" "discord" \
"Check the Home Assistant REST API as documented in your SOUL.md. Read indoor temperature and humidity sensors in each room (sensor.*temperature, sensor.*humidity). Comfortable range: temperature between 18C and 26C, humidity between 30 and 65 percent. For any room that is outside that range, report to Roger on Discord: the room name (entity_id), the reading, and whether it is too high or too low. Ask Roger what he wants to do about it. Do NOT change climate/HVAC, fans, or any entity yourself — only report and ask. If every room is within the comfortable range, reply with exactly [SILENT]."
# ---- 3. Morning briefing (daily at 08:00, always delivered) ----
create "morning-briefing" "0 8 * * *" "discord" \
# ---- 3. Morning briefing (daily at 08:00 local = 06:00 UTC, always delivered) ----
create "morning-briefing" "0 6 * * *" "discord" \
"Produce a morning home briefing for Roger using the Home Assistant REST API as documented in your SOUL.md. Include, in short bullet form: (1) today's calendar events from calendar.* entities for today (time + title); (2) open tasks from todo.* task lists; (3) overnight energy use — total energy consumed overnight from energy / utility_meter sensors, plus the current power draw; (4) outdoor and indoor temperature and humidity, and the current weather from weather.*; (5) occupancy status from zone.home — report its state if it exists; if zone.home is missing or unavailable, state 'occupancy: unknown — zone.home not configured' rather than claiming no one is home; (6) any sensors currently in an alert or unavailable state. Keep the message under 1800 chars. Always deliver (no [SILENT])."
# ---- 4. Evening briefing (daily at midnight, always delivered) ----
create "evening-briefing" "0 0 * * *" "discord" \
# ---- 4. Evening briefing (daily at midnight local = 22:00 UTC, always delivered) ----
create "evening-briefing" "0 22 * * *" "discord" \
"Produce an evening home status report for Roger using the Home Assistant REST API as documented in your SOUL.md. Report on: (1) lights currently turned on (light.* with state 'on'); (2) sockets and switches currently turned on (switch.* with state 'on'); (3) current energy use / power draw; (4) indoor temperature and humidity per room. List the specific entity_ids and their values. Then ask Roger which of the on devices/lights he wants turned off. Do NOT turn anything off yourself — only report and ask. Keep the message under 1800 chars. Always deliver (no [SILENT])."
echo "Done. Listing all cron jobs:"

View File

@@ -63,7 +63,8 @@ spec:
DISCORD_BOT_TOKEN DISCORD_HOME_CHANNEL \
GATEWAY_ALLOW_ALL_USERS DISCORD_FREE_RESPONSE_CHANNELS \
HERMES_DASHBOARD HERMES_DASHBOARD_BASIC_AUTH_USERNAME \
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET; do
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET \
API_SERVER_KEY; do
eval "v=\${$k:-}"
[ -n "$v" ] && echo "$k=$v" >> /opt/data/.env
done
@@ -103,6 +104,23 @@ spec:
# it lives here rather than in the Secret. The SOUL.md uses $HOMEASSISTANT_URL.
- name: HOMEASSISTANT_URL
value: "http://home-assistant.home-assistant:80"
# Expose the Hermes gateway HTTP API on port 8642 (the Service routes
# 80 → 8642). Used by Open WebUI and other in-cluster clients to talk
# to the agent as an OpenAI-compatible endpoint.
- name: API_SERVER_ENABLED
value: "true"
# Bind the gateway API on all interfaces so other pods can reach it
# via the Service (default is 127.0.0.1/loopback, which is unreachable
# cross-pod). Auth is still enforced via API_SERVER_KEY.
- name: API_SERVER_HOST
value: "0.0.0.0"
# Distinct model id advertised on GET /v1/models. Without this both
# agents report "hermes-agent" and Open WebUI dedupes them, hiding one.
- name: API_SERVER_MODEL_NAME
value: "home-manager"
# NOTE: API_SERVER_KEY comes from the hermes-env Secret (via envFrom)
# and is also seeded into /opt/data/.env by the init container. Clients
# (e.g. Open WebUI) authenticate with `Authorization: Bearer <key>`.
# HERMES_WRITE_SAFE_ROOT is intentionally unset → defaults to HERMES_HOME
# (/opt/data). The home agent has no git workspace to write to, so the
# tighter default is correct (memory/skills/scripts live under /opt/data).
@@ -118,7 +136,8 @@ spec:
cpu: "1000m"
livenessProbe:
# Probe the dashboard port (9119, always enabled via HERMES_DASHBOARD=1
# and binds 0.0.0.0). The gateway API on 8642 is off by default.
# and binds 0.0.0.0). The gateway API on 8642 is enabled via
# API_SERVER_ENABLED=true above.
tcpSocket:
port: 9119
initialDelaySeconds: 90

View File

@@ -0,0 +1,109 @@
---
# Home Assistant configuration backup.
#
# This CronJob copies Home Assistant backup tarballs from the config PVC
# (/config/backups) to the Synology NAS over SSH (rsync), so you have an
# off-PVC copy of your configuration.
#
# Prerequisite 1 (in the Home Assistant UI):
# Enable automatic backups so tarballs land in /config/backups:
# Settings -> System -> Backups -> (menu) -> Schedule backups
# Recommended: daily backup, e.g. "Every day at 04:00", keep the last 7.
#
# Prerequisite 2 (manual, once - secrets are NOT committed to Git):
# Create the NAS SSH credentials secret in the cluster:
#
# kubectl create secret generic home-assistant-backup-nas \
# --from-literal=NAS_USER=<nas-ssh-user> \
# --from-literal=NAS_HOST=10.88.30.10 \
# --from-literal=NAS_PORT=22 \
# --from-literal=NAS_PATH='<shared-folder-path>/home-assistant-backups' \
# --from-file=ssh-privatekey=$HOME/.ssh/id_rsa_nas \
# -n home-assistant
#
# Also add the NAS to known_hosts (so rsync doesn't prompt):
#
# ssh-keyscan -p 22 -H 10.88.30.10 > /tmp/nas_known_hosts
# kubectl create configmap home-assistant-backup-known-hosts \
# --from-file=known_hosts=/tmp/nas_known_hosts \
# -n home-assistant
#
# The NAS_HOST above (10.88.30.10) matches the NAS IP used by the nas-proxy
# service in this repo. Adjust NAS_PATH/credentials to your Synology share.
apiVersion: batch/v1
kind: CronJob
metadata:
name: home-assistant-backup
namespace: home-assistant
spec:
schedule: "30 4 * * *" # daily at 04:30 (after the HA 04:00 backup)
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: alpine:3.20
env:
- name: NAS_USER
valueFrom:
secretKeyRef:
name: home-assistant-backup-nas
key: NAS_USER
- name: NAS_HOST
valueFrom:
secretKeyRef:
name: home-assistant-backup-nas
key: NAS_HOST
- name: NAS_PORT
valueFrom:
secretKeyRef:
name: home-assistant-backup-nas
key: NAS_PORT
- name: NAS_PATH
valueFrom:
secretKeyRef:
name: home-assistant-backup-nas
key: NAS_PATH
command: ["/bin/sh", "-c"]
args:
- |
set -e
apk add --no-cache rsync openssh-client
mkdir -p ~/.ssh
cp /ssh-keys/ssh-privatekey ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
cp /known-hosts/known_hosts ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
echo "Syncing /config/backups -> ${NAS_USER}@${NAS_HOST}:${NAS_PATH}/"
rsync -a --delete -e "ssh -p ${NAS_PORT} -o StrictHostKeyChecking=yes" \
/config/backups/ "${NAS_USER}@${NAS_HOST}:${NAS_PATH}/"
echo "Backup sync complete."
volumeMounts:
- name: config
mountPath: /config
readOnly: true
- name: ssh-keys
mountPath: /ssh-keys
readOnly: true
- name: known-hosts
mountPath: /known-hosts
readOnly: true
volumes:
- name: config
persistentVolumeClaim:
claimName: home-assistant-config
- name: ssh-keys
secret:
secretName: home-assistant-backup-nas
items:
- key: ssh-privatekey
path: ssh-privatekey
- name: known-hosts
configMap:
name: home-assistant-backup-known-hosts

View File

@@ -55,20 +55,17 @@ spec:
spec:
containers:
- name: home-assistant
image: ghcr.io/home-assistant/home-assistant:stable
image: ghcr.io/home-assistant/home-assistant:2026.7.4
resources:
requests:
memory: "512Mi"
memory: "256Mi"
limits:
memory: "1Gi"
memory: "512Mi"
ports:
- containerPort: 8123
volumeMounts:
- name: config
mountPath: /config
- name: configuration
mountPath: /config/configuration.yaml
subPath: configuration.yaml
- name: localtime
mountPath: /etc/localtime
readOnly: true
@@ -82,6 +79,38 @@ spec:
- NET_ADMIN
- NET_RAW
- SYS_ADMIN
# Seed configuration.yaml from the ConfigMap onto the writable PVC so that
# Home Assistant has a writable configuration.yaml (required for the UI to
# create/save scripts, helpers, reloads, etc.). The ConfigMap stays the
# GitOps source of truth: it is re-copied on every pod start. UI-managed
# entities (scripts, automations, integrations) live in /config/.storage
# on the PVC and are never overwritten by this step.
initContainers:
- name: seed-config
image: busybox:1.36
command: ["/bin/sh", "-c"]
args:
- |
set -e
echo "Seeding /config/configuration.yaml from ConfigMap..."
cp /config-cm/configuration.yaml /config/configuration.yaml
echo "Done."
volumeMounts:
- name: config
mountPath: /config
- name: configuration
mountPath: /config-cm
readOnly: true
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- raspberrypi
hostNetwork: true
volumes:
- name: config

View File

@@ -178,7 +178,14 @@ data:
run (Hermes disables that). Do not try.
7. **Talk like an engineer.** Short, concrete, with resource names and
queries. No filler. When you fixed something, say what you did in one line.
8. **Respect GitOps.** If an app is `OutOfSync`/`Degraded`, check whether a
8. **Language: Catalan by default.** Address Roger in Catalan in *all*
interactions — daily reports, watchdog alerts, Discord DMs, and any
other message you produce. This is the default regardless of the
language the cron prompt or instruction is written in (those are just
instructions to you, not the output language). Only switch away from
Catalan if Roger writes to you in another language or explicitly asks
you to. Never default to English or Italian.
9. **Respect GitOps.** If an app is `OutOfSync`/`Degraded`, check whether a
commit is stuck. Don't hand-edit resources — fix the source repo.
## How you reach Roger

View File

@@ -98,14 +98,21 @@ spec:
create "argocd-sync-health" "every 6h" "discord" \
"Check ArgoCD app health using the API documented in your SOUL.md. If every app is Synced and Healthy, reply [SILENT]. Otherwise list the OutOfSync or Degraded apps with their status. If an app is OutOfSync and you believe a recent git push caused it, you may trigger a sync via the API. Do NOT hand-edit resources to fix them — fix the source repo."
create "cert-expiry" "0 9 * * *" "discord" \
create "cert-expiry" "0 7 * * *" "discord" \
"Check certificate expiry using the Prometheus API documented in your SOUL.md. Alert on any certificate expiring in under 21 days, with its name and namespace. If none, reply [SILENT]."
create "node-resource-drift" "every 1d" "discord" \
"Check node resources using the Prometheus API documented in your SOUL.md. Alert if any node is NotReady, or if any node has CPU over 90 percent or memory over 90 percent. Otherwise reply [SILENT]."
# ---- Daily report (always delivered) ----
create "daily-cluster-report" "0 8 * * *" "discord" \
# NOTE: Hermes' cron runs in UTC (no TZ set on the container). The
# cluster is at UTC+2, so fixed daily times are expressed in UTC and
# shifted back 2h from the intended local wall-clock time:
# 08:00 local -> 0 6 (daily cluster report)
# 09:00 local -> 0 7 (cert expiry check)
# Relative schedules (every 6h / every 1h / every 1d) are
# timezone-independent.
create "daily-cluster-report" "0 6 * * *" "discord" \
"Produce a daily cluster report for Roger using the HTTP APIs documented in your SOUL.md. Include: (1) node count and Ready/NotReady status per node, (2) top 5 pods by CPU and by memory, (3) count of pods not Running grouped by namespace, (4) any ArgoCD apps that are OutOfSync or Degraded, (5) any certificates expiring within 30 days, (6) recent error/crash activity: IMPORTANT context discipline — do NOT fetch raw log lines across the cluster over 24h — that overflows your context window. First run a LogQL aggregation that returns only counts: sum by (namespace, pod) (count_over_time({namespace=~\".+\"} |~ \"(?i)error|panic|crash|oom|backoff\" [24h])). Parse the counts with jq and summarize the top few noisiest pods by namespace/pod:count. If one stands out, fetch at most 5 sample lines for THAT pod only (limit=5) to identify the cause. Never paste more than a handful of lines total. Keep the whole report under 1800 chars. Always deliver (no [SILENT])."
echo "Done. Listing all cron jobs:"

View File

@@ -89,7 +89,8 @@ spec:
DISCORD_ALLOW_ALL_USERS DISCORD_FREE_RESPONSE_CHANNELS \
GITEA_TOKEN GITEA_REPO_URL ARGOCD_API_TOKEN ARGOCD_SERVER \
HERMES_DASHBOARD HERMES_DASHBOARD_BASIC_AUTH_USERNAME \
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET; do
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD HERMES_DASHBOARD_BASIC_AUTH_SECRET \
API_SERVER_KEY; do
eval "v=\${$k:-}"
[ -n "$v" ] && echo "$k=$v" >> /opt/data/.env
done
@@ -132,6 +133,23 @@ spec:
# with yolo:true, approvals.mode:off, and the agent having no k8s RBAC.
- name: HERMES_WRITE_SAFE_ROOT
value: "/"
# Expose the Hermes gateway HTTP API on port 8642 (the Service routes
# 80 → 8642). Used by Open WebUI and other in-cluster clients to talk
# to the agent as an OpenAI-compatible endpoint.
- name: API_SERVER_ENABLED
value: "true"
# Bind the gateway API on all interfaces so other pods can reach it
# via the Service (default is 127.0.0.1/loopback, which is unreachable
# cross-pod). Auth is still enforced via API_SERVER_KEY.
- name: API_SERVER_HOST
value: "0.0.0.0"
# Distinct model id advertised on GET /v1/models. Without this both
# agents report "hermes-agent" and Open WebUI dedupes them, hiding one.
- name: API_SERVER_MODEL_NAME
value: "platform-engineer"
# NOTE: API_SERVER_KEY comes from the hermes-env Secret (via envFrom)
# and is also seeded into /opt/data/.env by the init container. Clients
# (e.g. Open WebUI) authenticate with `Authorization: Bearer <key>`.
volumeMounts:
- name: data
mountPath: /opt/data
@@ -146,7 +164,8 @@ spec:
cpu: "1000m"
livenessProbe:
# Probe the dashboard port (9119, always enabled via HERMES_DASHBOARD=1
# and binds 0.0.0.0). The gateway API on 8642 is off by default.
# and binds 0.0.0.0). The gateway API on 8642 is enabled via
# API_SERVER_ENABLED=true above.
tcpSocket:
port: 9119
initialDelaySeconds: 90