Files
SolarManager/README.md
T
adminandClaude Opus 5 0a37d8d763 BYD-Speicher direkt aus der BMU auslesen
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>
2026-09-15 16:16:07 +02:00

237 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).