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>"]
|
||||
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"]
|
||||
@@ -25,7 +26,7 @@ flowchart LR
|
||||
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/>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>"]
|
||||
WSB["<b>wsMQTTbridge.py</b>"]
|
||||
RAIN["<b>gatherRainData.py</b>"]
|
||||
@@ -36,6 +37,7 @@ flowchart LR
|
||||
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
|
||||
@@ -44,7 +46,7 @@ flowchart LR
|
||||
METEO -->|HTTPS| RAIN
|
||||
|
||||
SOLARLOG -->|zisterne| MGR
|
||||
MGR -->|"EnergyFlow · skoda<br/>skoda_ladepunkte"| SOLARLOG
|
||||
MGR -->|"EnergyFlow · skoda<br/>skoda_ladepunkte · byd<br/>byd_zellen"| SOLARLOG
|
||||
|
||||
MGR -->|"solarManager/#"| BROKER
|
||||
WPB -->|"wattpilot/#"| BROKER
|
||||
@@ -57,7 +59,7 @@ flowchart LR
|
||||
classDef quelle fill:#eef4fb,stroke:#7f9dc0
|
||||
classDef skript fill:#fff6e5,stroke:#d0a548
|
||||
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 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
|
||||
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
|
||||
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.
|
||||
@@ -147,6 +158,7 @@ von Hand, wenn sich am Bestand etwas geändert hat.
|
||||
| `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` |
|
||||
|
||||
|
||||
@@ -47,3 +47,10 @@ intervall_minuten = 30
|
||||
; Messwerte geleert, und es wird nicht mehr bewaessert - eine retained "0,0 mm"
|
||||
; von gestern wuerde den Runner sonst giessen lassen.
|
||||
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)
|
||||
else:
|
||||
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
|
||||
publish(value,topic+"/"+key)
|
||||
else:
|
||||
|
||||
+8
-2
@@ -16,6 +16,7 @@ import gatherWaterData
|
||||
import gatherHeaterData
|
||||
import gatherShellyEM3DataEG
|
||||
import gatherShellyEM3DataUG
|
||||
import gatherBYDData
|
||||
import datetime
|
||||
from dateutil import tz
|
||||
from suntime import Sun, SunTimeException
|
||||
@@ -156,6 +157,7 @@ rtData = {"P_Load":0.0,
|
||||
"waterHeight":0,
|
||||
"waterTemp": 0.0,
|
||||
"inverters":[InvData(),InvData(),InvData(),InvData(),InvData(),InvData(),InvData(),InvData()],
|
||||
"byd":gatherBYDData.BYDData(), # direkt aus der BMU, siehe gatherBYDData.py
|
||||
"P_AC": 0.0
|
||||
}
|
||||
avgData = dict(rtData)
|
||||
@@ -268,7 +270,7 @@ async def repeat(interval):
|
||||
rtData["inverters"][0].p_PV.append(0)
|
||||
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),
|
||||
gatherDTUBIData.gatherData(),
|
||||
gatherOpenDTUData.gatherData(),
|
||||
@@ -280,8 +282,10 @@ async def repeat(interval):
|
||||
rtData["evPowerOG"], rtData["evPlugOG"],
|
||||
rtData["P_PV"]/1000.0, rtData["P_Grid"]/1000.0,
|
||||
rtData["wbWh"], rtData["wbogWh"]),
|
||||
gatherBYDData.gatherData(),
|
||||
asyncio.sleep(interval),
|
||||
)
|
||||
rtData["byd"] = byd
|
||||
rtData["evSOC"] = skoda.soc
|
||||
rtData["evRange"] = skoda.range_km
|
||||
rtData["evRemChrgTime"] = skoda.chgRemMin
|
||||
@@ -400,7 +404,9 @@ async def repeat(interval):
|
||||
rtData["P_WR"] = inv.p_wr
|
||||
rtData["ibatt"] = inv.ibatt
|
||||
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["pwrMaxPct"] = inv.pwrMaxPct
|
||||
if((rtData["P_PV"]+rtData["P_Akku"]) != 0):
|
||||
|
||||
Reference in New Issue
Block a user