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>
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 mo … so |
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.