Meine automations.yaml war klammheimlich zu einem 60-KB-Monster gewachsen. 2.144 Zeilen. 63 Automationen hintereinander in einer einzigen Datei, im Schnitt etwa 34 Zeilen pro Stück. Durchscrollen war eine Qual, Editieren machte nervös, und die eine Heizungsautomation zu finden, die ich brauchte, hieß Volltextsuche und ein Stoßgebet. Die Datei funktionierte tadellos — das war das Problem. Es gab keinen Druck, sie anzufassen, und jeden Grund, es nicht zu tun, denn in dem Moment, in dem du eine laufende Config refaktorierst, riskierst du, klammheimlich die Automation zu verlieren, die die Heizung am Einfrieren oder den Alarm am Taubwerden hindert.
Das ist also eine kleine Geschichte über ein Refactoring, das ich fast nicht gemacht hätte, und über das eine, das mich ihm vertrauen ließ: Ich schrieb ein 40-Zeilen-Skript, um die Datei zu splitten, und dann ein zweites, längeres Skript, dessen einzige Aufgabe war zu beweisen, dass das erste nichts verloren hatte.
Warum eine Datei irgendwann nicht mehr skaliert
Home Assistant bindet Automationen über eine einzige Zeile in der configuration.yaml ein: automation: !include automations.yaml. Dieses eine Include ist bequem — bis es das nicht mehr ist. Mit 63 Automationen darin hatten sich von selbst ein paar Muster gebildet. Heizung war mit Abstand das größte Thema — 21 Automationen, ein Drittel der ganzen Datei, die 11 Thermostate mit Tag- und Nachtmodus und raumweisen Overrides steuern. Alarm war der nächste Brocken mit 13. Dann ein Cluster rund um den AC·THOR-Leistungssteller, eine Handvoll Kamera- und Erkennungsregeln, zeitbasiertes Schalten, System-Hausarbeit und ein langer Schwanz an Einzelfällen.
Nichts von dieser Struktur war sichtbar. Es waren einfach 2.144 Zeilen in der Reihenfolge, in der ich sie hinzugefügt hatte. Ich wollte die Heizungsregeln in einer Heizungsdatei und die Möglichkeit, genau den Ausschnitt zu öffnen, an dem ich gerade arbeitete.
Ein 40-Zeilen-Splitter, der nach Namen klassifiziert
Der Splitter ist bewusst langweilig. Es sind etwa 40 Zeilen reines Standardbibliotheks-Python — nur yaml, os und ein defaultdict. Er lädt die YAML-Liste, läuft jede Automation ab, entscheidet, in welchen Eimer sie gehört, und schreibt pro Eimer eine Datei in ein automations/-Verzeichnis.
Und hier die ehrliche Macke: Die Klassifizierung läuft nicht über ein Tag- oder Metadatenfeld, weil es keins gibt. Es ist Substring-Matching auf dem deutschen alias der Automation. Enthält der Alias "Heizung", ist es Heizung; "Alarm" geht zu alarm; "AC Thor" (in allen Schreibweisen mit Bindestrich) zu ac_thor; Kamera- und Erkennungswörter zu cameras; Push- und Notification-Wörter zu notifications; das Zeitsteuerungs-Vokabular zu time_control; Sync- und Erreichbarkeitswörter zu system; und alles, was keine Regel erwischt, fällt in einen misc-Eimer. Das ist String-Matching, keine Metadaten, und das sage ich lieber klar, als es als etwas Schlaueres zu verkaufen.
categories = defaultdict(list)
for automation in automations:
alias = automation.get('alias', 'Unknown')
if 'Heizung' in alias:
categories['heating'].append(automation)
elif 'Alarm' in alias:
categories['alarm'].append(automation)
elif 'AC Thor' in alias or 'AC-Thor' in alias:
categories['ac_thor'].append(automation)
# ... cameras / notifications / time_control / system / misc
else:
categories['misc'].append(automation)
for category, autos in categories.items():
with open(f'automations/{category}.yaml', 'w') as f:
yaml.dump(autos, f, default_flow_style=False,
allow_unicode=True, sort_keys=False)Zwei dieser Dump-Argumente sind wichtiger, als sie aussehen. allow_unicode=True hält die deutschen Umlaute intakt, statt sie in Escape-Sequenzen zu verstümmeln, sodass ein Name wie "Büro" den Durchlauf übersteht. Und sort_keys=False bewahrt die ursprüngliche Schlüsselreihenfolge innerhalb jeder Automation — Trigger, Bedingung und Aktion bleiben in der Reihenfolge, in der ich sie geschrieben habe, statt zu sinnlosem Alphabet sortiert zu werden.
Acht Eimer, die die Daten selbst gewählt haben
Ich hatte mir die Acht nicht vorgenommen — ich hatte halb damit gerechnet, ein gutes Dutzend Dateien nach Gefühl von Hand zu schnitzen. Der eingecheckte Splitter wirft acht Kategoriedateien aus: heating, alarm, ac_thor, cameras, notifications, time_control, system und misc. Acht Eimer sind das, was die Daten wollten, keine Zahl, die ich aufgezwungen habe. Die Verteilung ist auf eine Weise schief, die man wirklich gerne sieht, sobald sie aufgeschlüsselt ist: Heizung sind 21 Automationen (33 %), Alarm 13 (21 %), AC·THOR 5 (8 %), und dann wird es schnell dünn bis hinunter zu Paaren und Einzelstücken. Nach geschätzter Größe landet die Heizungsdatei bei rund 700 Zeilen allein, Alarm bei rund 440, cameras bei rund 270, ac_thor bei rund 170. Die Heizungsdatei ist größer als meine gesamte Config, als ich dieses Projekt angefangen habe.
Diese Unwucht ist die eigentliche Erkenntnis. Eine 700-zeilige Heizungsdatei, die 11 Thermostate mit fast identischer Tag-/Nacht-/Anwesenheitslogik steuert, schreit förmlich danach, in eine Handvoll parametrisierter Skripte vorlagisiert zu werden — das ist das offensichtliche nächste Refactoring, und jetzt, wo es isoliert ist, sehe ich endlich seine Form. Die AC·THOR-Datei dagegen kam sauber und in sich geschlossen heraus: Temperaturüberwachung, Leistungssteuerung und ein bisschen Systemerkennung, alles an einem Ort.
Der Validator ist der eigentliche Held
Eine laufende Config zu splitten ist nur so vertrauenswürdig wie dein Beweis, dass nichts herausgefallen ist. Bevor ich also eine einzige Zeile der configuration.yaml änderte, schrieb ich ein zweites Skript — etwa 160 Zeilen — dessen ganzer Zweck ist, die Originaldatei und jede Split-Datei neu einzulesen und zu prüfen, dass der Durchlauf verlustfrei ist. Es führt fünf Checks aus. Eins: Die Gesamtzahl der Automationen stimmt überein. Zwei: Die Menge der eindeutigen Aliase stimmt überein, ohne dass eine Automation fehlt oder erfunden wurde. Drei: Doppelte Aliase sind zwischen Original und Split identisch. Vier: Jeder Automations-id-Wert stimmt als Menge überein. Fünf: ein tiefer Dictionary-Vergleich pro Alias, um stille Inhaltsdrift zu fangen.
if original_count == split_count:
print(f"COUNT CHECK: {split_count} automations (matches original)")
missing_in_split = set(original_aliases) - set(split_aliases)
extra_in_split = set(split_aliases) - set(original_aliases)
# IDs compared as sets; per-alias deep compare flags content drift
for alias in original_alias_set:
if alias in split_by_alias and original_by_alias[alias] != split_by_alias[alias]:
content_mismatches.append(alias)Das Detail, auf das ich am stolzesten bin, ist die bewusste Trennung zwischen Fehlern und Warnungen. Eine abweichende Anzahl, ein fehlender Alias, ein zusätzlicher Alias, eine Abweichung in der id-Menge — das sind harte Fehler, und das Skript beendet sich mit 1 und weigert sich, das neue Layout abzusegnen. Aber der tiefe Vergleich pro Automation wird als Warnung behandelt, nicht als Fehler, und es beendet sich trotzdem mit 0. Das ist eine bewusste Ermessensentscheidung: Wenn du YAML durch die Bibliothek neu serialisierst, formatieren sich die Bytes um — der Quoting-Stil verschiebt sich, manches Flow klappt zu Block zusammen — aber die geparste Struktur ist funktional identisch. Würde ich jede Umformatierung als Fehler behandeln, würde der Validator bei jedem sauberen Split schreien, und ich würde lernen, ihn zu ignorieren. Also bricht eine verlorene Automation das Ganze ab; ein kosmetisches Neu-Quoting bekommt nur eine Notiz. Der Validator darf eine Meinung dazu haben, welche Unterschiede wirklich zählen.
Die Ein-Zeilen-Belohnung
Mit grünem Validator ist die Umstellung fast antiklimaktisch. Du tauschst eine Zeile in der configuration.yaml — aus !include automations.yaml wird !include_dir_merge_list automations/ — und Home Assistant fügt nun jede Datei in diesem Verzeichnis zu einer Automationsliste zusammen, genau so, als wären sie noch aneinandergehängt.
Dann lädst du ohne kompletten Neustart neu: erst ein ha core check, um die Config zu validieren, dann ein Aufruf des homeassistant.reload-Dienstes gegen die Automations-Entität — bei mir ist das ein kleiner POST an die REST-API unter etwas wie http://homeassistant.local:8123, kein Reboot, keine Ausfallzeit. Die Automationen kommen genau so zurück, wie sie waren, nur verteilt über acht lesbare Dateien statt einer 2.144-zeiligen Wand.
Wenn du eine Gewohnheit hieraus mitnimmst, dann eine Namenskonvention: Stell neuen Aliasen ihre Kategorie voran, etwa "Heating: <Raum> Nachtmodus", damit der Substring-Klassifizierer deterministisch bleibt, während die Config wächst. Der Splitter bleibt nur ehrlich, solange die Namen es sind. Der Rest davon, wie dieses Haus auf Home Assistant läuft — von der Docker-auf-Azure-Installation an aufwärts — baut auf genau dieser Art kleiner, vertretbarer Schritte auf, und der nächste ist endlich, diese 700-zeilige Heizungsdatei zu vorlagisieren.



