adminandClaude Opus 5 813af4e8a2 Tahoma: verlorene Funkbefehle erkennen und nachsenden
Der Funk von der Box zum Motor ist unbestaetigt. Die Box nimmt einen Befehl
an und meldet Erfolg, auch wenn er den Motor nie erreicht - dann faehrt der
Behang gar nicht. Erkennbar ist das allein daran, dass er hinterher nicht
dort steht, wo er stehen soll. Genau das ist jetzt die Bedingung fuer den
zweiten Versuch.

Der Ansatz stand schon in der Datei, konnte aber nicht wirken:

Die Endstellung wurde gegen Neigung 0 geprueft, obwohl gerade die
gewuenschte Neigung geschickt worden war. Damit lieferte die Wartefunktion
immer "nicht erreicht" - verlorener Befehl und geglueckte Fahrt sahen gleich
aus, jedes Kommando lief zweimal 120 Sekunden und ging zweimal raus.

aktion["actor_name"] gibt es im Auftrag nicht; der Name steht eine Ebene
hoeher. Der KeyError flog nach der Vorstufe, also nachdem "Neigung 0" schon
draussen war: der Behang fuhr auf Position und liess die Lamellen offen. 23
Mal im Protokoll, zuletzt am 21.09. um 18:49 an acht Jalousien.

Jetzt:

  _positionsZiel() leitet das Ziel auch fuer "up" und "down" ab (0 bzw. 100
  Prozent Schliessung). Ohne das haette ausgerechnet der haeufigste Befehl
  kein pruefbares Ziel und der Wiederholversuch liefe fuer ihn leer.

  _zieleErreicht() vergleicht mit zwei Prozentpunkten Spielraum: io-Motoren
  melden fuer befohlene 100 gern 99 oder 101. Ohne Toleranz gaelte eine
  geglueckte Fahrt als verloren. None heisst "ist egal" - damit funktionieren
  auch Rollladen ohne Lamellen und reine Neigungsbefehle.

  _fahren() haelt die Schleife an einer Stelle. Vor der Wiederholung wird
  noch einmal nachgesehen, weil der Stand nur alle zwei Sekunden gelesen wird
  und der Behang in der letzten Sekunde angekommen sein kann. Ohne pruefbares
  Ziel (stop, my, wink) wird einmal geschickt und nicht gewartet.

Wiederholt wird ausdruecklich immer, wenn das Ziel nicht erreicht ist - auch
wenn der Behang unterwegs war und woanders stehengeblieben ist. Die
Unterscheidung waere ueber core:MovingState moeglich und ist bewusst nicht
gewollt.

Geprueft ohne Schaltbefehle: Ziel erreicht -> einmal gesendet; Befehl
verloren -> zweimal; erster verloren, zweiter kommt an -> zweimal, Erfolg;
99 statt 100 -> einmal; Rollladen ohne Lamellen, stop und reine Neigung
jeweils richtig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 19:47:39 +02:00
2026-09-02 20:46:59 +02:00

SolarManager

Die Hintergrundprozesse des Hauses: Messwerte einsammeln, Wallbox und Heizung regeln, die Automatiken auswerten. Gegenstück ist das Web-Repo admin/Smart-Dashboard unter /volume1/web/smart — dort liegt die Anzeige, hier liegt alles, was ohne offenen Browser weiterlaufen muss.

Beide Seiten reden über zwei Dinge miteinander: den MQTT-Broker für alles Aktuelle und die Datenbanken für alles, was bleiben soll. Kein Prozess ruft einen anderen direkt auf.

Woher die Zahlen kommen

flowchart LR

  WR["Wechselrichter<br/><small>GoodWe · OpenDTU · DTU-BI</small>"]
  EM3["Stromzähler<br/><small>2× Shelly EM3</small>"]
  HEIZ["Heizung"]
  BYD["BYD-Speicher<br/><small>BMU</small>"]
  GOE["go-eCharger"]
  WPILOT["Wattpilot"]
  SKODA["MySkoda-API"]
  STATION["Wetterstation"]
  METEO["Open-Meteo"]
  VENTILE["Ventilsteuerungen<br/><small>2× ESP32</small>"]
  HAUSGER["Thermostate · Shellys · Schalter<br/><small>melden sich per<br/>Home-Assistant-Discovery</small>"]

  MGR["<b>solarManager.py</b><br/><small>gatherModbusData · gatherOpenDTUData<br/>gatherDTUBIData · gatherShellyEM3Data EG/UG<br/>gatherHeaterData · gatherSkodaData<br/>gatherBYDData · gatherWaterData<br/>charger_goE</small>"]
  WPB["<b>wattpilot_bruecke.py</b>"]
  WSB["<b>wsMQTTbridge.py</b>"]
  FCS["<b>gatherForecastData.py</b><br/><small>Open-Meteo</small>"]
  RAIN["<b>gatherRainData.py</b>"]

  BROKER{{"<b>MQTT-Broker</b>"}}
  SOLARLOG[("<b>solarLog</b>")]

  WR      -->|Modbus · HTTP| MGR
  EM3     -->|HTTP| MGR
  HEIZ    -->|HTTP| MGR
  BYD     -->|Modbus-RTU in TCP| MGR
  GOE     -->|HTTP| MGR
  WPILOT  -->|WebSocket| MGR
  SKODA   -->|HTTPS| MGR
  WPILOT  -->|WebSocket| WPB
  STATION -->|WebSocket| WSB
  METEO   -->|HTTPS| RAIN
  METEO   -->|HTTPS| FCS

  SOLARLOG -->|zisterne| MGR
  MGR -->|"EnergyFlow · skoda<br/>skoda_ladepunkte · byd<br/>byd_zellen"| SOLARLOG
  FCS -->|"weatherHours · weatherDays<br/>weatherTilted · weatherForecastLog"| SOLARLOG

  MGR  -->|"solarManager/#"| BROKER
  WPB  -->|"wattpilot/#"| BROKER
  WSB  -->|"weatherStation/#"| BROKER
  RAIN -->|"Wetter/Regen"| BROKER
  GOE  -->|"go-eCharger/#"| BROKER
  VENTILE -->|"Gartenwasser/#"| BROKER
  HAUSGER -->|"Raumtemp/# · Power_EG/#<br/>Power_UG/# · wasser/#"| BROKER

  classDef quelle fill:#eef4fb,stroke:#7f9dc0
  classDef skript fill:#fff6e5,stroke:#d0a548
  classDef speicher fill:#eaf5ee,stroke:#6fa981
  class WR,EM3,HEIZ,BYD,GOE,WPILOT,SKODA,STATION,METEO,VENTILE,HAUSGER quelle
  class MGR,WPB,WSB,RAIN,FCS skript
  class BROKER,SOLARLOG speicher

Fünf Prozesse holen aktiv etwas ab. Alles andere meldet sich von selbst: die Ventilsteuerungen, die Wallbox und jedes Gerät, das Home-Assistant-Discovery spricht, schreiben ohne Umweg auf den Broker. Für deren Messwerte ist also kein Skript zuständig — wer sie sucht, sucht am Gerät, nicht im Quelltext.

solarManager.py ist der Sonderfall: ein Prozess, aber neun Sammler-Module, jedes für eine Anlage. Sie laufen nicht einzeln, sondern werden importiert; ihr gemeinsames Ergebnis geht als ein Baum nach solarManager/#.

gatherBYDData liest die BMU der Batterie direkt (192.168.16.254:8080) und legt alles unter solarManager/byd/… ab: Ladestand und Gesundheit laut BMU, Zellspannungen (zellen, 128 Werte in mV, 16 je Modul), Temperaturen (temperaturen, 64 Werte, 8 je Modul), Spreizung, Ausgleich, Fehlerbits und die Gesamtzähler. ok fällt auf 0, wenn zehn Minuten nichts kam. Das Netzwerkmodul der BMU startet etwa alle 102 Sekunden neu und antwortet nur kurz danach — der Sammler klopft deshalb jede Sekunde an und bekommt so rund alle 100 Sekunden einen vollständigen Satz. Einzelheiten im Kopf des Moduls und, ausführlich, im Web-Repository unter doku/byd.md.

gatherForecastData holt alle zwanzig Minuten die Wettervorhersage von Open-Meteo und schreibt sie nach solarLog (weatherHours, weatherDays, weatherForecastLog, weatherTilted) — Grundlage des Meteogramms der Weboberfläche. Es ist der einzige Sammler, der nichts nach MQTT veröffentlicht: ein Meteogramm liest niemand im Sekundentakt.

Es löscht nichts. Die Tabellen sind zugleich ein Archiv — aus der Einstrahlung und EnergyFlow_hourly.pv_kwh soll später eine eigene Ertragsprognose gerechnet werden. wetterarchiv_nachtragen.py hat sie einmalig aus der ERA5-Schnittstelle von Open-Meteo bis zum 02.04.2022 zurückgefüllt. Einzelheiten stehen im Web-Repository unter doku/meteogramm.md.

Die Zisterne fällt aus der Reihe — ihr Stand steht in solarLog, und gatherWaterData liest ihn von dort. Die Daten laufen also durch die Datenbank hindurch von einem Prozess zum nächsten.

Wer sie benutzt

flowchart TB

  BROKER{{"<b>MQTT-Broker</b>"}}
  SOLARLOG[("<b>solarLog</b><br/><small>Verlauf</small>")]
  HOMEMESH[("<b>homeMesh</b><br/><small>Geräte · Automatiken</small>")]

  RUNNER["<b>autoaction_runner.py</b><br/><small>SolarManager</small>"]
  KALENDER["<b>fetch_calendar.py</b><br/><small>SolarManager · Cronjob, jährlich</small>"]
  DISCOVERY["<b>device_discovery.py</b><br/><small>Web · von Hand gestartet</small>"]
  AJAX["<b>ajax/*.php</b><br/><small>Web</small>"]
  BROWSER["<b>js/solar/*.js</b><br/><small>Web · im Browser</small>"]

  HAUS["Rollläden · Licht · Schalter<br/><small>Tahoma · WLED · Shelly</small>"]
  VENTILE["Ventilsteuerungen"]
  FERIEN["openholidaysapi.org"]

  BROKER <-->|"Messwerte ↓ Kommandos ↑"| RUNNER
  HOMEMESH <-->|"Regelwerk ↓ Verlauf ↑"| RUNNER
  RUNNER -.->|"HTTP · WLED · Tahoma"| HAUS
  BROKER -.->|"Gartenwasser/…/set"| VENTILE

  FERIEN --> KALENDER
  KALENDER -->|"calendar_days"| HOMEMESH

  BROKER -->|"homeassistant/#"| DISCOVERY
  HAUS -->|"mDNS · Tahoma"| DISCOVERY
  DISCOVERY -->|"Geräte, Messwerte, Befehle"| HOMEMESH

  SOLARLOG --> AJAX
  HOMEMESH --> AJAX
  AJAX <--> BROWSER
  AJAX -.->|Kommandos| BROKER
  BROKER -->|wss| BROWSER

  classDef skript fill:#fff6e5,stroke:#d0a548
  classDef speicher fill:#eaf5ee,stroke:#6fa981
  classDef geraet fill:#eef4fb,stroke:#7f9dc0
  class RUNNER,DISCOVERY,AJAX,BROWSER,KALENDER skript
  class BROKER,SOLARLOG,HOMEMESH speicher
  class HAUS,VENTILE,FERIEN geraet

Durchgezogen fließen Daten, gestrichelt gehen Befehle.

Der Browser bekommt seine laufenden Werte direkt vom Broker über wss://mqtt.nas.el-wa.org, nicht über PHP. Über PHP läuft nur, was der Broker nicht weiß: der Verlauf aus solarLog und alles aus homeMesh — und umgekehrt jeder Knopfdruck, denn schalten darf nur der Server.

device_discovery.py ist kein Dauerläufer. Es sucht per mDNS nach Shellys und WLEDs, hört die Discovery-Nachrichten auf homeassistant/# mit, fragt die Tahoma-Box und legt daraus die Geräte in homeMesh an. Gestartet wird es von Hand, wenn sich am Bestand etwas geändert hat.

Wem welche Daten gehören

Daten wird gefüllt von wird gelesen von
solarManager/# solarManager.py Browser, Runner
wattpilot/# wattpilot_bruecke.py Browser, Runner
weatherStation/# wsMQTTbridge.py Browser, Runner
Wetter/Regen gatherRainData.py Runner, Browser
go-eCharger/# die Wallbox selbst Browser, Runner
Gartenwasser/# die beiden ESP32 Browser, Runner
Raumtemp/#, Power_*/#, wasser/# die Geräte selbst Browser, Runner
homeassistant/# die Geräte selbst device_discovery.py
solarLog solarManager.py ajax/*.php, gatherWaterData
byd, byd_zellen (in solarLog) gatherBYDData.py, alle 5 bzw. 15 Minuten Web-Repo (solarLog_byd.sql)
weatherHours, weatherDays, weatherTilted, weatherForecastLog (in solarLog) gatherForecastData.py, alle 20 Minuten; nichts wird geloescht Web-Repo (solarLog_weather.sql)
homeMesh device_discovery.py, Runner, Web-Editor Runner, ajax/*.php
skoda.conf Einstellungen → Fahrzeug (restricted/skodaKeys.php) oder von Hand gatherSkodaData.py, ajax/skodaCmd.php

Was wann startet

Prozess gestartet von
solarManager.py startSolarServer.sh (Aufgabenplaner, beim Hochfahren)
autoActions/autoaction_runner.py dito
gatherRainData.py dito
gatherForecastData.py dito
wsMQTTbridge.py startMQTTbridge.sh
wattpilot_bruecke.py startWattpilotMQTT.sh
autoActions/fetch_calendar.py Cronjob, einmal im Jahr
device_discovery.py (Web-Repo) von Hand

startSolarServer.sh beendet eine schon laufende Instanz, bevor es neu startet — beim Runner ist das wichtig, zwei Instanzen würden jedes Kommando doppelt schicken.

Der Wecker ist keiner mehr

wecker.py gibt es nicht mehr. Es hat genau das getan, was der AutoAction-Runner ohnehin kann — nur mit allem doppelt: eigener Datenbank alarm, eigenem Feiertags- und Ferienkalender, eigenem UDP-Log auf Port 13377, eigener Endlosschleife und einem eigenen Startskript samt nächtlichem Neustart um drei.

Punkt für Punkt hatte der Runner die bessere Fassung schon:

wecker.py Automatik
alarmtime.time_1 Bedingung „Uhrzeit um 05:50"
Wochentagsspalten moso weekdays, eine Bitmaske
is_holiday() gegen alarm.feiertage on_holiday, gegen calendar_days
is_school_holiday() gegen alarm.ferien on_vacation, ebenso
alarmtime.wecked = heute cond_met, die steigende Flanke
fünf HTTP-Versuche im Abstand von vier Sekunden der Versand, ein Faden je Gerät
send_log() per UDP automation_log und autoActions.log

Der Kalender war der auffälligste Teil davon: alarm.feiertage und alarm.ferien standen neben calendar_days, das fetch_calendar.py einmal im Jahr von openholidaysapi.org holt. Zwei Kalender, die dasselbe wissen müssen, gehen früher oder später auseinander.

Umgezogen ist die eine Weckzeit, die scharf war: 05:50, Montag bis Freitag, nicht in den Ferien, nicht an Feiertagen, WLED-Preset 5 („Wakeup") auf MenasHimmel. Die drei anderen Zeilen in alarmtime standen auf onoff = -1. wecker_zu_automatik.sql legt die Automatik an und beschreibt im Kopf, wie dasselbe im Dashboard von Hand geht.

Damit ist die Weckzeit dort einstellbar, wo alles andere auch eingestellt wird — unter „Automatismen", Etage EG. Die Datenbank alarm ist gelöscht, und config.ini.example hat keinen Abschnitt [alarm] mehr. Ein übriggebliebener [alarm] in einer echten config.ini stört nicht, liest aber auch niemand.

Konfiguration

Zugangsdaten und Standort stehen in config.ini (Vorlage: config.ini.example), gelesen über konfig.py; der Runner hat seine eigene unter autoActions/. Die Zugangsschlüssel der MyŠkoda-API stehen getrennt in skoda.conf (Vorlage: skoda.conf.example), weil auch die Weboberfläche sie liest und im Reiter „Fahrzeug" neue einträgt. Alle drei sind per .gitignore ausgenommen — nichts davon gehört in den Quelltext.

Werkzeuge

Skript wofür
skoda_test.py prüft Zerlegung und Kontingent-Buchführung von gatherSkodaData.py ohne Fahrzeug und ohne Netz
skoda_ladepunkte_nachtragen.py holt den Wallbox-Verlauf vergangener Ladungen aus EnergyFlow nach skoda_ladepunkte, solange er dort noch nicht ausgedünnt ist (--probe schreibt nichts)
wecker_zu_automatik.sql hat den alten Wecker als Automatik angelegt; nur noch zum Nachlesen

Mehr zum Runner selbst, zum Aufbau einer Automatik und zu den Transporten steht in autoActions/README.md.

S
Description
No description provided
Readme
3 MiB
Languages
Python 87%
C 11.1%
Cython 0.9%
JavaScript 0.6%
Shell 0.2%
Other 0.2%