gatherBYDData.py fragt die BMU der HVM unter 192.168.16.254:8080 ab (BE-Connect-Protokoll, nach ioBroker.bydhvs): Ladestand und SOH laut BMU, 128 Zellspannungen, 64 Temperaturen, Spreizung, Ausgleich, Fehlerbits, Gesamtzaehler. Das Netzwerkmodul startet alle ~102 s neu und bedient nur die ersten Verbindungen danach - eigene Klopf-Schleife, ein Satz etwa alle 100 s. Werte unter solarManager/byd/#, Historie in byd und byd_zellen. tbatt kommt jetzt aus der BMU statt fest 0. mqttClient.publish legt ein einzelnes Dataclass-Objekt in rtData als Untertopics ab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
237 lines
10 KiB
Markdown
237 lines
10 KiB
Markdown
# 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
|
||
|
||
```mermaid
|
||
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>"]
|
||
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
|
||
|
||
SOLARLOG -->|zisterne| MGR
|
||
MGR -->|"EnergyFlow · skoda<br/>skoda_ladepunkte · byd<br/>byd_zellen"| 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 skript
|
||
class BROKER,SOLARLOG speicher
|
||
```
|
||
|
||
Vier 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.
|
||
|
||
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
|
||
|
||
```mermaid
|
||
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`) |
|
||
| **`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 |
|
||
| `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](autoActions/README.md).
|