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>
This commit is contained in:
@@ -17,6 +17,7 @@ flowchart LR
|
|||||||
WR["Wechselrichter<br/><small>GoodWe · OpenDTU · DTU-BI</small>"]
|
WR["Wechselrichter<br/><small>GoodWe · OpenDTU · DTU-BI</small>"]
|
||||||
EM3["Stromzähler<br/><small>2× Shelly EM3</small>"]
|
EM3["Stromzähler<br/><small>2× Shelly EM3</small>"]
|
||||||
HEIZ["Heizung"]
|
HEIZ["Heizung"]
|
||||||
|
BYD["BYD-Speicher<br/><small>BMU</small>"]
|
||||||
GOE["go-eCharger"]
|
GOE["go-eCharger"]
|
||||||
WPILOT["Wattpilot"]
|
WPILOT["Wattpilot"]
|
||||||
SKODA["MySkoda-API"]
|
SKODA["MySkoda-API"]
|
||||||
@@ -25,7 +26,7 @@ flowchart LR
|
|||||||
VENTILE["Ventilsteuerungen<br/><small>2× ESP32</small>"]
|
VENTILE["Ventilsteuerungen<br/><small>2× ESP32</small>"]
|
||||||
HAUSGER["Thermostate · Shellys · Schalter<br/><small>melden sich per<br/>Home-Assistant-Discovery</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/>gatherWaterData · charger_goE</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>"]
|
WPB["<b>wattpilot_bruecke.py</b>"]
|
||||||
WSB["<b>wsMQTTbridge.py</b>"]
|
WSB["<b>wsMQTTbridge.py</b>"]
|
||||||
RAIN["<b>gatherRainData.py</b>"]
|
RAIN["<b>gatherRainData.py</b>"]
|
||||||
@@ -36,6 +37,7 @@ flowchart LR
|
|||||||
WR -->|Modbus · HTTP| MGR
|
WR -->|Modbus · HTTP| MGR
|
||||||
EM3 -->|HTTP| MGR
|
EM3 -->|HTTP| MGR
|
||||||
HEIZ -->|HTTP| MGR
|
HEIZ -->|HTTP| MGR
|
||||||
|
BYD -->|Modbus-RTU in TCP| MGR
|
||||||
GOE -->|HTTP| MGR
|
GOE -->|HTTP| MGR
|
||||||
WPILOT -->|WebSocket| MGR
|
WPILOT -->|WebSocket| MGR
|
||||||
SKODA -->|HTTPS| MGR
|
SKODA -->|HTTPS| MGR
|
||||||
@@ -44,7 +46,7 @@ flowchart LR
|
|||||||
METEO -->|HTTPS| RAIN
|
METEO -->|HTTPS| RAIN
|
||||||
|
|
||||||
SOLARLOG -->|zisterne| MGR
|
SOLARLOG -->|zisterne| MGR
|
||||||
MGR -->|"EnergyFlow · skoda<br/>skoda_ladepunkte"| SOLARLOG
|
MGR -->|"EnergyFlow · skoda<br/>skoda_ladepunkte · byd<br/>byd_zellen"| SOLARLOG
|
||||||
|
|
||||||
MGR -->|"solarManager/#"| BROKER
|
MGR -->|"solarManager/#"| BROKER
|
||||||
WPB -->|"wattpilot/#"| BROKER
|
WPB -->|"wattpilot/#"| BROKER
|
||||||
@@ -57,7 +59,7 @@ flowchart LR
|
|||||||
classDef quelle fill:#eef4fb,stroke:#7f9dc0
|
classDef quelle fill:#eef4fb,stroke:#7f9dc0
|
||||||
classDef skript fill:#fff6e5,stroke:#d0a548
|
classDef skript fill:#fff6e5,stroke:#d0a548
|
||||||
classDef speicher fill:#eaf5ee,stroke:#6fa981
|
classDef speicher fill:#eaf5ee,stroke:#6fa981
|
||||||
class WR,EM3,HEIZ,GOE,WPILOT,SKODA,STATION,METEO,VENTILE,HAUSGER quelle
|
class WR,EM3,HEIZ,BYD,GOE,WPILOT,SKODA,STATION,METEO,VENTILE,HAUSGER quelle
|
||||||
class MGR,WPB,WSB,RAIN skript
|
class MGR,WPB,WSB,RAIN skript
|
||||||
class BROKER,SOLARLOG speicher
|
class BROKER,SOLARLOG speicher
|
||||||
```
|
```
|
||||||
@@ -68,11 +70,20 @@ 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
|
deren Messwerte ist also **kein Skript** zuständig — wer sie sucht, sucht am
|
||||||
Gerät, nicht im Quelltext.
|
Gerät, nicht im Quelltext.
|
||||||
|
|
||||||
`solarManager.py` ist der Sonderfall: ein Prozess, aber acht
|
`solarManager.py` ist der Sonderfall: ein Prozess, aber neun
|
||||||
Sammler-Module, jedes für eine Anlage. Sie laufen nicht einzeln, sondern
|
Sammler-Module, jedes für eine Anlage. Sie laufen nicht einzeln, sondern
|
||||||
werden importiert; ihr gemeinsames Ergebnis geht als ein Baum nach
|
werden importiert; ihr gemeinsames Ergebnis geht als ein Baum nach
|
||||||
`solarManager/#`.
|
`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
|
Die Zisterne fällt aus der Reihe — ihr Stand steht in `solarLog`, und
|
||||||
`gatherWaterData` liest ihn von dort. Die Daten laufen also durch die
|
`gatherWaterData` liest ihn von dort. Die Daten laufen also durch die
|
||||||
Datenbank hindurch von einem Prozess zum nächsten.
|
Datenbank hindurch von einem Prozess zum nächsten.
|
||||||
@@ -147,6 +158,7 @@ von Hand, wenn sich am Bestand etwas geändert hat.
|
|||||||
| `Raumtemp/#`, `Power_*/#`, `wasser/#` | die Geräte selbst | Browser, Runner |
|
| `Raumtemp/#`, `Power_*/#`, `wasser/#` | die Geräte selbst | Browser, Runner |
|
||||||
| `homeassistant/#` | die Geräte selbst | `device_discovery.py` |
|
| `homeassistant/#` | die Geräte selbst | `device_discovery.py` |
|
||||||
| **`solarLog`** | `solarManager.py` | `ajax/*.php`, `gatherWaterData` |
|
| **`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` |
|
| **`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` |
|
| `skoda.conf` | Einstellungen → Fahrzeug (`restricted/skodaKeys.php`) oder von Hand | `gatherSkodaData.py`, `ajax/skodaCmd.php` |
|
||||||
|
|
||||||
|
|||||||
@@ -47,3 +47,10 @@ intervall_minuten = 30
|
|||||||
; Messwerte geleert, und es wird nicht mehr bewaessert - eine retained "0,0 mm"
|
; Messwerte geleert, und es wird nicht mehr bewaessert - eine retained "0,0 mm"
|
||||||
; von gestern wuerde den Runner sonst giessen lassen.
|
; von gestern wuerde den Runner sonst giessen lassen.
|
||||||
max_alter_stunden = 3
|
max_alter_stunden = 3
|
||||||
|
|
||||||
|
; ===========================================================================
|
||||||
|
; BYD-Speicher (gatherBYDData.py)
|
||||||
|
; ===========================================================================
|
||||||
|
; Adresse der BMU. Ab Werk 192.168.16.254; der Abschnitt darf fehlen.
|
||||||
|
[byd]
|
||||||
|
host = 192.168.16.254
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
"""BYD Battery-Box HVM direkt an der BMU auslesen.
|
||||||
|
|
||||||
|
Der Gen24 liefert ueber Modbus nur, was er selbst von der Batterie wissen
|
||||||
|
muss: Ladestand, Spannung, Strom. Alles darunter - 128 Zellspannungen, 64
|
||||||
|
Temperaturfuehler, Gesundheit (SOH), Gesamtzaehler, Ausgleich - kennt nur die
|
||||||
|
BMU. Sie spricht auf Port 8080 dasselbe Protokoll wie die BE-Connect-App:
|
||||||
|
Modbus-RTU in TCP verpackt. Ablauf und Byte-Lagen stammen aus dem
|
||||||
|
ioBroker-Adapter bydhvs (MIT, github.com/christianh17/ioBroker.bydhvs).
|
||||||
|
|
||||||
|
Drei Eigenheiten bestimmen den Aufbau:
|
||||||
|
|
||||||
|
Alles in EINER Verbindung, Anfrage 0 zuerst. Eine neue Verbindung, die
|
||||||
|
gleich mit Anfrage 1 beginnt, schliesst die BMU wortlos.
|
||||||
|
|
||||||
|
Das Netzwerkmodul (Hi-Link HLK-RM08K) startet etwa alle 102 Sekunden neu,
|
||||||
|
unabhaengig davon, ob jemand fragt. Waehrend des Neustarts laufen
|
||||||
|
Verbindungen rund 15 Sekunden ins Leere; danach bedient es nur die ersten
|
||||||
|
ein, zwei Verbindungen, alle spaeteren schliesst es sofort mit 0 Bytes.
|
||||||
|
Das Fenster ist also nur wenige Sekunden breit (beobachtet am 15.09.2026).
|
||||||
|
Der Sammler klopft deshalb in einer eigenen Schleife jede _KLOPFEN
|
||||||
|
Sekunde mit kurzem Verbindungs-Timeout an - der 3-Sekunden-Takt des
|
||||||
|
Managers waere dafuer zu grob. Geht eine Verbindung durch, laeuft der
|
||||||
|
ganze Zyklus in ihr; die haelt das Modul offen, auch ueber die Messpause.
|
||||||
|
Nach einem Erfolg ruht die Schleife _RUHE Sekunden bis kurz vor dem
|
||||||
|
naechsten Neustart.
|
||||||
|
|
||||||
|
Die Lade-/Entladezaehler stehen bei dieser HVM wortvertauscht (low word
|
||||||
|
zuerst). Mit der Lesart des Adapters kaemen 31 Millionen kWh heraus.
|
||||||
|
|
||||||
|
Wie die anderen langsamen Sammler blockiert gatherData() nie: die Schleife
|
||||||
|
laeuft als Hintergrund-Task, zurueck kommt immer der letzte Stand.
|
||||||
|
|
||||||
|
Historie: alle 5 Minuten eine Zeile in byd, alle 15 Minuten die Zellen
|
||||||
|
in byd_zellen (siehe solarLog_byd.sql im Web-Repository).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import mysql.connector as mc
|
||||||
|
|
||||||
|
import konfig
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_HOST = konfig.wert("byd", "host", "192.168.16.254")
|
||||||
|
_PORT = 8080
|
||||||
|
_KLOPFEN = 1 # s zwischen zwei Versuchen, solange das Fenster zu ist
|
||||||
|
_RUHE = 80 # s nach einem Erfolg - das naechste Fenster kommt erst nach ~102 s
|
||||||
|
_VERBINDEN = 1.5 # s Timeout fuers Verbinden; im Neustart antwortet niemand
|
||||||
|
_ZEITLIMIT = 6 # s je Antwort
|
||||||
|
_MESSPAUSE = 8 # s, die die BMU nach "Zellmessung starten" braucht
|
||||||
|
_VERALTET = 600 # s ohne Erfolg, danach gilt der Stand als nicht mehr aktuell
|
||||||
|
_DB_ZEILE = 300 # s zwischen zwei Zeilen in byd
|
||||||
|
_DB_ZELLEN = 900 # s zwischen zwei Zeilen in byd_zellen
|
||||||
|
|
||||||
|
_ANFRAGE = {
|
||||||
|
"stamm": bytes.fromhex("010300000066c5e0"),
|
||||||
|
"echt": bytes.fromhex("01030500001984cc"),
|
||||||
|
"typ": bytes.fromhex("010300100003040e"),
|
||||||
|
"messen": bytes.fromhex("0110055000020400018100f853"),
|
||||||
|
"status": bytes.fromhex("010305510001d517"),
|
||||||
|
"zellen": bytes.fromhex("01030558004104e5"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bit j in Paket 1, Byte 29/30 - Wortlaut aus dem Adapter, uebersetzt.
|
||||||
|
_FEHLER = [
|
||||||
|
"Zu warm beim Laden", "Zu kalt beim Laden", "Ueberstrom beim Entladen",
|
||||||
|
"Ueberstrom beim Laden", "Hauptstromkreis gestoert", "Kurzschlussalarm",
|
||||||
|
"Zellen ungleich", "Stromsensor gestoert", "Batteriespannung zu hoch",
|
||||||
|
"Batteriespannung zu niedrig", "Zellspannung zu hoch",
|
||||||
|
"Zellspannung zu niedrig", "Spannungssensor gestoert",
|
||||||
|
"Temperatursensor gestoert", "Zu warm beim Entladen",
|
||||||
|
"Zu kalt beim Entladen",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BYDData:
|
||||||
|
ok: int = 0 # 1 = Stand juenger als _VERALTET
|
||||||
|
stand: int = 0 # Unix-Zeit des letzten vollstaendigen Zyklus
|
||||||
|
seriennummer: str = ""
|
||||||
|
firmware: str = ""
|
||||||
|
module: int = 0
|
||||||
|
soc: float = 0.0 # % laut BMU
|
||||||
|
soh: float = 0.0 # %
|
||||||
|
spannung: float = 0.0 # V am Batterieausgang
|
||||||
|
strom: float = 0.0 # A, positiv = laden
|
||||||
|
leistung: float = 0.0 # W, positiv = laden
|
||||||
|
tMin: float = 0.0 # degC ueber alle Fuehler
|
||||||
|
tMax: float = 0.0
|
||||||
|
zelleMin: int = 0 # mV
|
||||||
|
zelleMax: int = 0
|
||||||
|
zelleMinNr: int = 0 # 1..128
|
||||||
|
zelleMaxNr: int = 0
|
||||||
|
spreizung: int = 0 # mV
|
||||||
|
ausgleich: int = 0 # Zahl der Zellen, die gerade ausgeglichen werden
|
||||||
|
ausgleichRoh: str = "" # "0x" + 32 Hex-Zeichen, Bitlage noch nicht bestaetigt;
|
||||||
|
# das Praefix haelt mqttClient davon ab, es als Zahl zu lesen
|
||||||
|
fehler: int = 0
|
||||||
|
fehlertext: str = ""
|
||||||
|
geladen: float = 0.0 # kWh seit Inbetriebnahme
|
||||||
|
entladen: float = 0.0
|
||||||
|
zellen: List[int] = field(default_factory=list) # mV, 16 je Modul
|
||||||
|
temperaturen: List[int] = field(default_factory=list) # degC, 8 je Modul
|
||||||
|
|
||||||
|
|
||||||
|
ret = BYDData()
|
||||||
|
_task = None
|
||||||
|
_letzteZeile = 0.0
|
||||||
|
_letzteZellen = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _crc_ok(p):
|
||||||
|
crc = 0xFFFF
|
||||||
|
for b in p:
|
||||||
|
crc ^= b
|
||||||
|
for _ in range(8):
|
||||||
|
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
|
||||||
|
return crc == 0
|
||||||
|
|
||||||
|
|
||||||
|
class _Abgewiesen(Exception):
|
||||||
|
"""Die BMU hat die Verbindung ohne Antwort geschlossen - Fenster zu."""
|
||||||
|
|
||||||
|
|
||||||
|
async def _frage(reader, writer, name, pause=0.2):
|
||||||
|
await asyncio.sleep(pause)
|
||||||
|
writer.write(_ANFRAGE[name])
|
||||||
|
await writer.drain()
|
||||||
|
buf = b""
|
||||||
|
while True:
|
||||||
|
teil = await asyncio.wait_for(reader.read(1024), _ZEITLIMIT)
|
||||||
|
if not teil:
|
||||||
|
raise _Abgewiesen("%s: geschlossen nach %d Bytes" % (name, len(buf)))
|
||||||
|
buf += teil
|
||||||
|
if len(buf) < 3:
|
||||||
|
continue
|
||||||
|
if buf[1] & 0x80:
|
||||||
|
n = 5
|
||||||
|
elif buf[1] == 0x10:
|
||||||
|
n = 8
|
||||||
|
else:
|
||||||
|
n = buf[2] + 5
|
||||||
|
if len(buf) >= n:
|
||||||
|
p = buf[:n]
|
||||||
|
if not _crc_ok(p):
|
||||||
|
raise IOError("%s: Pruefsumme falsch" % name)
|
||||||
|
if p[1] & 0x80:
|
||||||
|
raise IOError("%s: BMU meldet Ausnahme %d" % (name, p[2]))
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
def _i16(b, o):
|
||||||
|
return struct.unpack_from(">h", b, o)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _u16(b, o):
|
||||||
|
return struct.unpack_from(">H", b, o)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _u32_vertauscht(b, o):
|
||||||
|
"""Zwei Worte, das niedrige zuerst."""
|
||||||
|
return (_u16(b, o + 2) << 16) | _u16(b, o)
|
||||||
|
|
||||||
|
|
||||||
|
async def _zyklus():
|
||||||
|
reader, writer = await asyncio.wait_for(
|
||||||
|
asyncio.open_connection(_HOST, _PORT), _VERBINDEN)
|
||||||
|
try:
|
||||||
|
p0 = await _frage(reader, writer, "stamm", 0)
|
||||||
|
p1 = await _frage(reader, writer, "echt")
|
||||||
|
p2 = await _frage(reader, writer, "typ")
|
||||||
|
module = p0[36] % 16
|
||||||
|
hvm = p2[5] == 1
|
||||||
|
nZellen = module * (16 if hvm else 32)
|
||||||
|
nTemps = module * (8 if hvm else 12)
|
||||||
|
|
||||||
|
await _frage(reader, writer, "messen")
|
||||||
|
await asyncio.sleep(_MESSPAUSE)
|
||||||
|
await _frage(reader, writer, "status", 0)
|
||||||
|
p5 = await _frage(reader, writer, "zellen")
|
||||||
|
p6 = await _frage(reader, writer, "zellen")
|
||||||
|
p7 = await _frage(reader, writer, "zellen")
|
||||||
|
p8 = await _frage(reader, writer, "zellen")
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
|
||||||
|
zellen = [_i16(p5, 101 + 2 * i) for i in range(16)]
|
||||||
|
zellen += [_i16(p6, 5 + 2 * i) for i in range(min(64, nZellen - 16))]
|
||||||
|
zellen += [_i16(p7, 5 + 2 * i) for i in range(min(48, nZellen - 80))]
|
||||||
|
temps = [p7[103 + i] for i in range(min(30, nTemps))]
|
||||||
|
temps += [p8[5 + i] for i in range(min(34, nTemps - 30))]
|
||||||
|
|
||||||
|
d = BYDData()
|
||||||
|
d.seriennummer = p0[3:22].decode("ascii", "replace")
|
||||||
|
d.firmware = "BMS V%d.%d" % (p0[31], p0[32])
|
||||||
|
d.module = module
|
||||||
|
d.soc = _i16(p1, 3)
|
||||||
|
d.soh = _i16(p1, 9)
|
||||||
|
d.strom = _i16(p1, 11) / 10
|
||||||
|
d.spannung = _u16(p1, 35) / 100
|
||||||
|
d.leistung = round(d.strom * d.spannung)
|
||||||
|
d.fehler = _u16(p1, 29)
|
||||||
|
d.fehlertext = "; ".join(t for j, t in enumerate(_FEHLER) if d.fehler & (1 << j))
|
||||||
|
d.geladen = _u32_vertauscht(p1, 37) / 10
|
||||||
|
d.entladen = _u32_vertauscht(p1, 41) / 10
|
||||||
|
d.zellen = zellen
|
||||||
|
d.temperaturen = temps
|
||||||
|
gueltig = [z for z in zellen if 2000 < z < 4000]
|
||||||
|
if gueltig:
|
||||||
|
d.zelleMin, d.zelleMax = min(gueltig), max(gueltig)
|
||||||
|
d.zelleMinNr = zellen.index(d.zelleMin) + 1
|
||||||
|
d.zelleMaxNr = zellen.index(d.zelleMax) + 1
|
||||||
|
d.spreizung = d.zelleMax - d.zelleMin
|
||||||
|
if temps:
|
||||||
|
d.tMin, d.tMax = min(temps), max(temps)
|
||||||
|
else:
|
||||||
|
d.tMin, d.tMax = _i16(p1, 17), _i16(p1, 15)
|
||||||
|
d.ausgleichRoh = "0x" + p5[17:33].hex()
|
||||||
|
d.ausgleich = bin(int(d.ausgleichRoh, 16)).count("1")
|
||||||
|
d.stand = int(time.time())
|
||||||
|
d.ok = 1
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _speichern(d):
|
||||||
|
"""Historie schreiben, gedrosselt auf _DB_ZEILE / _DB_ZELLEN."""
|
||||||
|
global _letzteZeile, _letzteZellen
|
||||||
|
jetzt = time.time()
|
||||||
|
zeile = jetzt - _letzteZeile >= _DB_ZEILE
|
||||||
|
zellen = jetzt - _letzteZellen >= _DB_ZELLEN
|
||||||
|
if not (zeile or zellen):
|
||||||
|
return
|
||||||
|
zeit = datetime.datetime.now().replace(microsecond=0)
|
||||||
|
try:
|
||||||
|
with mc.connect(**konfig.datenbank()) as verbindung:
|
||||||
|
with verbindung.cursor() as cursor:
|
||||||
|
if zeile:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO byd (datetime, soc, soh, spannung, strom, leistung, "
|
||||||
|
"t_min, t_max, zelle_min, zelle_max, zelle_min_nr, zelle_max_nr, "
|
||||||
|
"spreizung, ausgleich, fehler, geladen_kwh, entladen_kwh) "
|
||||||
|
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s);",
|
||||||
|
(zeit, d.soc, d.soh, d.spannung, d.strom, d.leistung,
|
||||||
|
d.tMin, d.tMax, d.zelleMin, d.zelleMax, d.zelleMinNr,
|
||||||
|
d.zelleMaxNr, d.spreizung, d.ausgleich, d.fehler,
|
||||||
|
d.geladen, d.entladen))
|
||||||
|
if zellen:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO byd_zellen (datetime, zellen, temperaturen, ausgleich) "
|
||||||
|
"VALUES (%s,%s,%s,%s);",
|
||||||
|
(zeit, json.dumps(d.zellen), json.dumps(d.temperaturen),
|
||||||
|
d.ausgleichRoh[2:]))
|
||||||
|
verbindung.commit()
|
||||||
|
if zeile:
|
||||||
|
_letzteZeile = jetzt
|
||||||
|
if zellen:
|
||||||
|
_letzteZellen = jetzt
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error("BYD: Historie nicht geschrieben: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
async def _schleife():
|
||||||
|
global ret
|
||||||
|
gewarnt = False
|
||||||
|
letzterErfolg = time.time()
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
d = await _zyklus()
|
||||||
|
except (_Abgewiesen, ConnectionError, asyncio.TimeoutError, OSError) as e:
|
||||||
|
# Ein zugesperrtes Fenster ist der Normalfall und kein Log wert.
|
||||||
|
# Erst wenn gar nichts mehr durchkommt, soll es jemand sehen.
|
||||||
|
_LOGGER.debug("BYD: %s: %s", type(e).__name__, e)
|
||||||
|
if not gewarnt and time.time() - letzterErfolg > _VERALTET:
|
||||||
|
_LOGGER.warning("BYD: seit %d Minuten keine Daten von %s (%s)",
|
||||||
|
_VERALTET // 60, _HOST, e)
|
||||||
|
gewarnt = True
|
||||||
|
if ret.ok and time.time() - ret.stand > _VERALTET:
|
||||||
|
ret.ok = 0
|
||||||
|
await asyncio.sleep(_KLOPFEN)
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("BYD: Antwort nicht verstanden")
|
||||||
|
await asyncio.sleep(_RUHE)
|
||||||
|
else:
|
||||||
|
ret = d
|
||||||
|
letzterErfolg = time.time()
|
||||||
|
if gewarnt:
|
||||||
|
_LOGGER.warning("BYD: Daten kommen wieder")
|
||||||
|
gewarnt = False
|
||||||
|
await asyncio.get_running_loop().run_in_executor(None, _speichern, d)
|
||||||
|
await asyncio.sleep(_RUHE)
|
||||||
|
|
||||||
|
|
||||||
|
async def gatherData() -> BYDData:
|
||||||
|
"""Startet beim ersten Aufruf die Schleife und gibt den letzten Stand zurueck."""
|
||||||
|
global _task
|
||||||
|
if _task is None or _task.done():
|
||||||
|
if _task is not None and not _task.cancelled() and _task.exception():
|
||||||
|
_LOGGER.error("BYD: Schleife beendet: %r - starte neu", _task.exception())
|
||||||
|
_task = asyncio.ensure_future(_schleife())
|
||||||
|
return ret
|
||||||
+5
-1
@@ -54,7 +54,11 @@ def publish(dict,topic="solarManager"):
|
|||||||
mqttClient.publish(topic+"/"+key,value)
|
mqttClient.publish(topic+"/"+key,value)
|
||||||
else:
|
else:
|
||||||
for key, value in dict.items():
|
for key, value in dict.items():
|
||||||
if hasattr(value, '__len__') and (not isinstance(value, str)):
|
# Ein einzelnes Dataclass-Objekt (z.B. rtData["byd"]) hat keine
|
||||||
|
# Laenge und fiel frueher in den Zahlenzweig - "value is no number".
|
||||||
|
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
||||||
|
publish(value,topic+"/"+key)
|
||||||
|
elif hasattr(value, '__len__') and (not isinstance(value, str)):
|
||||||
if dataclasses.is_dataclass(value): #make subtopics of dataclasses
|
if dataclasses.is_dataclass(value): #make subtopics of dataclasses
|
||||||
publish(value,topic+"/"+key)
|
publish(value,topic+"/"+key)
|
||||||
else:
|
else:
|
||||||
|
|||||||
+8
-2
@@ -16,6 +16,7 @@ import gatherWaterData
|
|||||||
import gatherHeaterData
|
import gatherHeaterData
|
||||||
import gatherShellyEM3DataEG
|
import gatherShellyEM3DataEG
|
||||||
import gatherShellyEM3DataUG
|
import gatherShellyEM3DataUG
|
||||||
|
import gatherBYDData
|
||||||
import datetime
|
import datetime
|
||||||
from dateutil import tz
|
from dateutil import tz
|
||||||
from suntime import Sun, SunTimeException
|
from suntime import Sun, SunTimeException
|
||||||
@@ -156,6 +157,7 @@ rtData = {"P_Load":0.0,
|
|||||||
"waterHeight":0,
|
"waterHeight":0,
|
||||||
"waterTemp": 0.0,
|
"waterTemp": 0.0,
|
||||||
"inverters":[InvData(),InvData(),InvData(),InvData(),InvData(),InvData(),InvData(),InvData()],
|
"inverters":[InvData(),InvData(),InvData(),InvData(),InvData(),InvData(),InvData(),InvData()],
|
||||||
|
"byd":gatherBYDData.BYDData(), # direkt aus der BMU, siehe gatherBYDData.py
|
||||||
"P_AC": 0.0
|
"P_AC": 0.0
|
||||||
}
|
}
|
||||||
avgData = dict(rtData)
|
avgData = dict(rtData)
|
||||||
@@ -268,7 +270,7 @@ async def repeat(interval):
|
|||||||
rtData["inverters"][0].p_PV.append(0)
|
rtData["inverters"][0].p_PV.append(0)
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
inv,dtubi,dtu,heat,chrg,pEG,pUG,skoda,slp = await asyncio.gather(
|
inv,dtubi,dtu,heat,chrg,pEG,pUG,skoda,byd,slp = await asyncio.gather(
|
||||||
gatherModbusData.get_runtime_data(estProduction, sunset),
|
gatherModbusData.get_runtime_data(estProduction, sunset),
|
||||||
gatherDTUBIData.gatherData(),
|
gatherDTUBIData.gatherData(),
|
||||||
gatherOpenDTUData.gatherData(),
|
gatherOpenDTUData.gatherData(),
|
||||||
@@ -280,8 +282,10 @@ async def repeat(interval):
|
|||||||
rtData["evPowerOG"], rtData["evPlugOG"],
|
rtData["evPowerOG"], rtData["evPlugOG"],
|
||||||
rtData["P_PV"]/1000.0, rtData["P_Grid"]/1000.0,
|
rtData["P_PV"]/1000.0, rtData["P_Grid"]/1000.0,
|
||||||
rtData["wbWh"], rtData["wbogWh"]),
|
rtData["wbWh"], rtData["wbogWh"]),
|
||||||
|
gatherBYDData.gatherData(),
|
||||||
asyncio.sleep(interval),
|
asyncio.sleep(interval),
|
||||||
)
|
)
|
||||||
|
rtData["byd"] = byd
|
||||||
rtData["evSOC"] = skoda.soc
|
rtData["evSOC"] = skoda.soc
|
||||||
rtData["evRange"] = skoda.range_km
|
rtData["evRange"] = skoda.range_km
|
||||||
rtData["evRemChrgTime"] = skoda.chgRemMin
|
rtData["evRemChrgTime"] = skoda.chgRemMin
|
||||||
@@ -400,7 +404,9 @@ async def repeat(interval):
|
|||||||
rtData["P_WR"] = inv.p_wr
|
rtData["P_WR"] = inv.p_wr
|
||||||
rtData["ibatt"] = inv.ibatt
|
rtData["ibatt"] = inv.ibatt
|
||||||
rtData["ubatt"] = inv.ubatt
|
rtData["ubatt"] = inv.ubatt
|
||||||
rtData["tbatt"] = 0
|
# Der Gen24 kennt keine Batterietemperatur; die BMU schon. Die
|
||||||
|
# Spalte battTemp in EnergyFlow stand bisher immer auf 0.
|
||||||
|
rtData["tbatt"] = byd.tMax if byd.ok else 0
|
||||||
rtData["crgMaxPct"] = inv.crgMaxPct
|
rtData["crgMaxPct"] = inv.crgMaxPct
|
||||||
#rtData["pwrMaxPct"] = inv.pwrMaxPct
|
#rtData["pwrMaxPct"] = inv.pwrMaxPct
|
||||||
if((rtData["P_PV"]+rtData["P_Akku"]) != 0):
|
if((rtData["P_PV"]+rtData["P_Akku"]) != 0):
|
||||||
|
|||||||
Reference in New Issue
Block a user