Nächtliche Gesundheitschecks für Home Assistant — GitHub Actions als Wächter

Wie ich eine nächtliche CI-Pipeline gebaut habe, die 22 Tests gegen mein Live-Home-Assistant-Setup ausführt und automatisch ein GitHub-Issue öffnet, wenn etwas nicht funktioniert — bevor ich es selbst bemerke.

#home-assistant #github-actions #testing #proxmox #automation
11. Mai 2026
Nächtliche Gesundheitschecks für Home Assistant — GitHub Actions als Wächter

Die Geschirrspüler-Benachrichtigung funktionierte monatelang perfekt. Dann eines Tages nicht mehr. Der notify.android_tv-Dienst war nach einem Home Assistant-Neustart still verschwunden, und ich erfuhr es erst, als ich in der Küche stand und auf einen Alarm wartete, der nie kam.

Das war das letzte Mal, dass ich von einer defekten Integration erfuhr, weil ich sie zufällig genau dann brauchte.

Warum ich nächtliche Tests für Home Assistant gebaut habe

Jede Nacht um 02:00 Uhr führt ein Skript alle meine Home-Assistant-Muster durch eine Testsuite und prüft, ob alles noch funktioniert. Wenn etwas fehlschlägt, öffnet sich automatisch ein GitHub-Issue mit dem genauen Fehler und einer Liste wahrscheinlicher Ursachen. Bis zum Morgen weiß ich, was kaputt gegangen ist und ungefähr warum.

Das Ganze läuft auf GitHub Actions. Die Tests sind normales Node.js. Keine Drittanbieter-Überwachungsdienste, keine Cloud-Abonnements, keine zusätzlichen Dashboards zum Prüfen.

Warum ein self-hosted Runner nötig ist

GitHubs Cloud-Runner können eine Home-Assistant-Instanz im lokalen Netzwerk nicht erreichen. Die Tests rufen die HA-REST-API und WebSocket direkt auf, daher muss der Runner im gleichen Netzwerk wie HA laufen.

Die Lösung: ein leichtgewichtiger Linux-Container auf dem Proxmox-Host, der bereits HA betreibt. Ein zusätzlicher LXC-Container — Debian 12, 1 vCPU, 512 MB RAM — als self-hosted GitHub-Actions-Runner registriert. Er startet automatisch mit Proxmox und kostet kaum Ressourcen.

Der Runner erhält ein Label (home-network), damit der Workflow nur auf Hardware läuft, die HA tatsächlich erreichen kann:

jobs:
  ha-tests:
    runs-on: [self-hosted, home-network]
    timeout-minutes: 5

Den Proxmox-LXC-Runner einrichten

Der Runner lebt in einem dedizierten LXC-Container auf demselben Proxmox-Host wie Home Assistant. Erstelle ihn über die Proxmox-UI: Create CT → Debian-12-Template, 1 vCPU, 512 MB RAM, Bridged-Netzwerk (vmbr0). Container starten, dann die Konsole öffnen.

Node.js installieren:

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

Den GitHub-Actions-Runner installieren. Gehe zu deinem Repository → Settings → Actions → Runners → New self-hosted runner und kopiere die von GitHub generierten Download- und Konfigurationsbefehle. Der Konfigurationsschritt ist der Ort, wo du Runner-Name und Labels setzt:

./config.sh \
  --url https://github.com/DEIN_USER/DEIN_REPO \
  --token DEIN_TOKEN \
  --name proxmox-runner \
  --labels home-network \
  --unattended

Das Label home-network verbindet diesen Runner mit dem runs-on: [self-hosted, home-network] des Workflows. Ohne es leitet GitHub den Job nicht an diese Maschine weiter.

Abschließend den Runner als systemd-Dienst installieren und starten, damit er Neustarts überlebt:

sudo ./svc.sh install
sudo ./svc.sh start

Wenn der Runner registriert und im Leerlauf ist, erscheint er unter Settings → Actions → Runners im Repository:

Registrierter GitHub-Self-hosted-Runner mit home-network-Label

Zugangsdaten: der richtige Weg

Die Tests benötigen ein langlebiges HA-Token. Der richtige Ansatz ist, es als GitHub-Actions-Secret zu speichern — niemals im Repository hardcoden.

In HA: Profil → Langlebige Zugriffstoken → Token erstellen

In GitHub: Repository → Settings → Secrets and variables → Actions → New repository secret

HA_TOKEN als Name. Der Workflow injiziert es als Umgebungsvariable:

env:
  HA_URL: http://homeassistant.local:8123
  HA_TOKEN: ${{ secrets.HA_TOKEN }}

Der Test-Helper liest beide aus der Umgebung:

const HA_URL = process.env.HA_URL;
const HA_TOKEN = process.env.HA_TOKEN;
if (!HA_URL || !HA_TOKEN) throw new Error('HA_URL and HA_TOKEN env vars required');

Repository-Struktur

Eine Workflow-Datei, Testdateien unter tests/ha/ und ein gemeinsamer Helper für alle HA-API-Aufrufe:

.
├── .github/
│   └── workflows/
│       └── ha-nightly.yml
├── tests/
│   └── ha/
│       ├── smoke.test.mjs
│       ├── notifications.test.mjs
│       ├── automations.test.mjs
│       ├── integrations.test.mjs
│       └── custom-cards.test.mjs
├── lib/
│   └── ha-client.mjs
└── package.json

package.json benötigt nur "type": "module" — keine Test-Framework-Abhängigkeit, da Node’s eingebautes node:test alles übernimmt.

Den nächtlichen Testlauf mit GitHub Actions planen

on:
  schedule:
    - cron: '0 1 * * *'   # 02:00 Uhr dänische Zeit (UTC+1)
  workflow_dispatch:        # manueller Trigger jederzeit verfügbar

workflow_dispatch bedeutet, dass ich es auch manuell aus dem GitHub-Actions-Tab auslösen kann, wenn ich nach Änderungen etwas überprüfen möchte.

Was die 22 Home Assistant-Tests prüfen

Die Testsuite verwendet Node’s eingebauten node:test-Runner — kein Jest, kein Mocha, keine zusätzlichen Abhängigkeiten. Jede Testdatei deckt einen Bereich ab.

Smoke-Test — antwortet HA überhaupt?

test('HA API ist erreichbar und läuft', async () => {
  const result = await haGet('/api/');
  assert.equal(result.message, 'API running.');
});

Einfach, aber entscheidend. Wenn HA ausgefallen ist oder das Token abgelaufen ist, schlägt alles andere hier zuerst mit einer klaren Meldung fehl.

Benachrichtigungsdienste — existieren die Telefon- und Tablet-Dienste wirklich?

test('notify.mobile_app_pixel_10 Dienst existiert', async () => {
  const services = await haGet('/api/services');
  const notifyDomain = services.find(s => s.domain === 'notify');
  assert.ok('mobile_app_pixel_10' in notifyDomain.services);
});

Dies fängt den Fall ab, wo die Companion-App neu installiert wurde, der Gerätename geändert wurde oder die Integration ausgefallen ist. Der Dienst kann ohne sichtbaren Fehler in HA verschwinden.

Automationen — sind welche defekt oder versehentlich deaktiviert?

test('keine Automation ist unavailable', async () => {
  const states = await haGet('/api/states');
  const broken = states.filter(
    s => s.entity_id.startsWith('automation.') && s.state === 'unavailable'
  );
  assert.deepEqual(broken.map(s => s.entity_id), []);
});

Es gibt auch spezifische Tests für EV-Ladeautomationen und Mähroboter-Zeitpläne — nicht nur ob sie existieren, sondern ob sie aktiviert sind. Eine versehentlich deaktivierte Automation ist funktional defekt, auch wenn HA sie als gesund anzeigt.

Integrationsspezifische Prüfungen — sieht die Kamera, was sie soll? Sind alle 5 Tado-Klimazonen vorhanden? Ist der Roborock erreichbar? Jede Integration erhält ihren eigenen Test, der verifiziert, dass die richtigen Entitäten existieren und nicht unavailable sind.

Der Custom-Cards-Test — der clevere

Dies ist der nützlichste Test in der Suite und der, über den ich am frohsten bin.

Home Assistant validiert die YAML-Struktur der Dashboard-Konfiguration, prüft aber nicht, ob das Custom-Card-JavaScript tatsächlich installiert ist. Wenn man in einem Dashboard auf custom:some-card verweist und die HACS-Komponente nicht installiert ist, zeigt der Browser eine generische “Configuration error”-Meldung. Kein Serverlog erfasst das. Keine HA-Benachrichtigung löst aus. Man sieht nur eine defekte Karte.

Der Test löst dies, indem er zwei Dinge kreuzt:

  1. Alle im Dashboard verwendeten custom:*-Kartentypen (per WebSocket abgerufen, rekursiv durch den gesamten Kartenbaum gelaufen)
  2. Alle in Lovelace registrierten JavaScript-Ressourcen (ebenfalls per WebSocket)
test('alle Custom Cards haben registrierte JS-Ressourcen', async () => {
  const [resources, dashboardPaths] = await Promise.all([
    getLovelaceResources(),
    getAllDashboardPaths(),
  ]);
  const resourceUrls = resources.map(r => r.url ?? '');

  for (const path of dashboardPaths) {
    const config = await getDashboard(path);
    const types = extractCustomTypes(config.views);

    for (const type of types) {
      const fragment = CARD_RESOURCE_MAP[type]; // z.B. 'mushroom', 'apexcharts-card'
      assert.ok(
        resourceUrls.some(url => url.includes(fragment)),
        `${type} — JS-Ressource nicht registriert`
      );
    }
  }
});

Die CARD_RESOURCE_MAP ordnet jeden Kartentyp dem URL-Fragment zu, das sein HACS-Paket registriert. Mushroom-Karten teilen sich alle eine JS-Datei (mushroom), daher werden alle 15+ Mushroom-Untertypen auf das gleiche Fragment gemappt.

Dies hat echte Probleme abgefangen: ein HA-Update, das HACS zurückgesetzt hat, eine manuelle Neuinstallation, die ein Paket ausgelassen hat, eine neue Karte, die ich einem Dashboard hinzugefügt und deren Ressource ich zu installieren vergessen hatte.

Was passiert, wenn ein Test fehlschlägt

Der letzte Schritt des Workflows läuft bei Fehlern und öffnet ein GitHub-Issue:

- name: Issue bei Fehlschlag öffnen
  if: failure()
  uses: actions/github-script@v7
  with:
    script: |
      const output = fs.readFileSync('test-output.txt', 'utf8').slice(0, 4000);
      const title = `[HA Health] Test regression — ${today}`;
      // Deduplizierung: überspringen wenn Titel bereits existiert
      await github.rest.issues.create({
        title,
        labels: ['ha-regression'],
        body: `## Test Output\n\`\`\`\n${output}\n\`\`\`\n\n## Häufige Ursachen\n...`,
      });

Das Issue enthält die genaue fehlgeschlagene Assertion, das Datum, einen Link zur vollständigen Ausführung, eine Liste häufiger Ursachen (Entität unavailable, umbenannte Entität, HA-Update, abgelaufenes Token) und den Befehl zum lokalen Ausführen der Tests.

Deduplizierung verhindert, dass dasselbe Problem über 30 Nächte 30 Issues öffnet, bevor ich es behebe.

Tests lokal ausführen

Wenn ein Issue öffnet, ist der schnellste Weg zur Diagnose, die Suite lokal gegen die Live-Instanz zu starten:

npm install
HA_URL=http://homeassistant.local:8123 \
HA_TOKEN=dein_token_hier \
node --test tests/ha/*.test.mjs

Die Ausgabe ist TAP-formatiert. Fehlgeschlagene Tests zeigen die genaue Assertion, die brach, und den tatsächlichen vs. erwarteten Wert.

Was das wirklich abfängt

Seit der Einrichtung hat es gefangen:

  • Den notify.android_tv-Dienst, der nach einem HA-Neustart verschwand (das failed_unload-Problem, das in einem anderen Beitrag beschrieben wird)
  • Eine Mähroboter-Automation, die deaktiviert wurde, als ich eine andere bearbeitete und versehentlich den Toggle traf
  • Ein HACS-Update, das die Custom-Card-Ressourcen zurücksetzte und still drei Dashboard-Ansichten zerbrach
  • Die Škoda-Elroq-Integration, die nach einem HA-Update ihre Entitäten verlor, als die Integration ihre interne Benennung änderte

Keine dieser Situationen wäre offensichtlich geworden, bevor ich genau das brauchte, was kaputt war. Jetzt sind es GitHub-Issues um 02:05 Uhr.

Warum automatisiertes Monitoring für Home Assistant wichtig ist

Neunundzwanzig Jahre Infrastrukturarbeit bedeuten, dass ich Heimautomatisierung genauso behandle wie Produktionssysteme: Dinge gehen still kaputt, zu ungünstigen Zeiten, und der einzige Weg, es zu wissen bevor es wichtig wird, ist kontinuierliches Prüfen. Ein nächtlicher Testlauf gegen eine Live-HA-Instanz ist das Minimum lebensfähiger Überwachung für ein Setup, von dem man wirklich abhängig ist.

Die Konzepte sind einfach genug, um sie für jedes HA-Setup anzupassen — selbst nur mit dem Smoke-Test und dem Automations-Gesundheitscheck anzufangen, gibt einem etwas Wertvolles.