Skoda: mehrere Schluessel, Wallbox-Ladeverlauf, Stecker als 1/0

- gatherSkodaData.py liest bis zu drei API-Schluessel und nutzt sie reihum;
  das Kontingent gilt gemessen fuers ganze Konto, daher feste Aufteilung
  14 Abruf / 4 Befehle / 2 Reserve
- nach einem Befehl aus der Weboberflaeche wird einmal ausser der Reihe
  nachgesehen
- Wallbox-Verlauf waehrend einer Ladung nach skoda_ladepunkte, dazu
  skoda_ladepunkte_nachtragen.py fuer Ladungen aus EnergyFlow
- evPlug ist jetzt ein Wahrheitswert: bool("no car") war immer True
- skoda.conf.example beschreibt das gemeinsame Format

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 09:29:57 +02:00
co-authored by Claude Opus 5
parent 153658df81
commit 8c62455b2c
5 changed files with 858 additions and 127 deletions
+131
View File
@@ -0,0 +1,131 @@
"""Wallbox-Verlauf vergangener Ladungen aus EnergyFlow nach skoda_ladepunkte.
Den laufenden Verlauf schreibt gatherSkodaData.py selbst, im Minutentakt.
Fuer alles davor - und fuer Zeiten, in denen der Manager nicht lief - gibt
es nur EnergyFlow, und das wird nach zwoelf Monaten ausgeduennt. Dieses
Skript holt die Fuenf-Minuten-Werte heraus, solange sie noch da sind.
python3 skoda_ladepunkte_nachtragen.py 2026-09-11 2026-09-14
python3 skoda_ladepunkte_nachtragen.py 2026-09-11 2026-09-14 --probe
Ohne --probe wird geschrieben. Es darf beliebig oft laufen: nachgetragene
Zeilen im Zeitraum werden vorher entfernt, und wo der Manager selbst schon
Punkte geschrieben hat, traegt es nichts nach - die sind genauer.
EnergyFlow kennt keinen Zaehlerstand, nur Leistungen. wh_total wird deshalb
gerechnet: die Leistung ueber die Zeit aufsummiert und dann an die beiden
Zaehlerstaende angeglichen, die die Tabelle skoda vor und nach der Ladung
kennt. Fehlt einer davon, bleibt wh_total leer. Jede so entstandene Zeile
traegt quelle = 'energyflow'.
"""
import sys
import datetime
import mysql.connector as mc
import konfig
LUECKE = datetime.timedelta(hours=3) # wie LADUNG_LUECKE_S in ajax/skoda.php
RUHE_KW = 0.5 # wie _LADEPUNKT_KW in gatherSkodaData.py
WALLBOXEN = {
# Name: (Leistung in EnergyFlow, Zaehlerspalte in skoda)
"carport": ("PL1_EV + PL2_EV + PL3_EV", "wb_wh_total"),
"og": ("PL1_EVog + PL2_EVog + PL3_EVog", "wbog_wh_total"),
}
def bloecke(zeilen):
"""Zusammenhaengende Strecken mit Leistung, jeweils mit der Zeile danach."""
block = []
for i, (zeit, kw) in enumerate(zeilen):
if kw > RUHE_KW:
block.append((zeit, kw))
elif block:
block.append((zeit, 0.0)) # der Endpunkt, wie im Manager
yield block
block = []
if block:
yield block # laeuft noch - ohne Endpunkt
def zaehlerstand(cursor, spalte, zeit, richtung):
"""Zaehlerstand aus skoda direkt vor bzw. nach einem Zeitpunkt."""
if richtung < 0:
cursor.execute("SELECT datetime, " + spalte + " FROM skoda WHERE datetime <= %s "
"AND datetime >= %s AND " + spalte + " > 0 "
"ORDER BY datetime DESC LIMIT 1", (zeit, zeit - LUECKE))
else:
cursor.execute("SELECT datetime, " + spalte + " FROM skoda WHERE datetime >= %s "
"AND datetime <= %s AND " + spalte + " > 0 "
"ORDER BY datetime LIMIT 1", (zeit, zeit + LUECKE))
z = cursor.fetchone()
return int(z[1]) if z else None
def main():
if len(sys.argv) < 3:
print(__doc__)
sys.exit(1)
von = datetime.datetime.fromisoformat(sys.argv[1])
bis = datetime.datetime.fromisoformat(sys.argv[2])
probe = "--probe" in sys.argv
zugang = konfig.datenbank()
with mc.connect(host=zugang["host"], port=zugang["port"], user=zugang["user"],
password=zugang["password"], database=zugang["database"]) as v:
with v.cursor() as c:
neu = []
for name, (leistung, spalte) in WALLBOXEN.items():
c.execute("SELECT datetime, " + leistung + " FROM EnergyFlow "
"WHERE datetime BETWEEN %s AND %s ORDER BY datetime", (von, bis))
zeilen = [(z[0], float(z[1] or 0.0)) for z in c.fetchall()]
for block in bloecke(zeilen):
beginn, ende = block[0][0], block[-1][0]
c.execute("SELECT COUNT(*) FROM skoda_ladepunkte WHERE wallbox = %s "
"AND quelle = 'manager' AND datetime BETWEEN %s AND %s",
(name, beginn - datetime.timedelta(minutes=5), ende))
if c.fetchone()[0]:
print(name, beginn, "bis", ende, "- vom Manager schon erfasst, uebersprungen")
continue
# Leistung ueber die Zeit. Ein EnergyFlow-Wert ist das Mittel
# der fuenf Minuten, die er abschliesst.
summe, vorher, verlauf = 0.0, None, []
for zeit, kw in block:
dt = (zeit - vorher).total_seconds() if vorher else 300.0
summe += kw * dt / 3.6 if kw > 0 else 0.0
verlauf.append((zeit, kw, summe))
vorher = zeit
anfang = zaehlerstand(c, spalte, beginn - datetime.timedelta(minutes=5), -1)
schluss = zaehlerstand(c, spalte, ende, +1)
if anfang is not None and schluss is not None and schluss > anfang and summe > 0:
faktor = (schluss - anfang) / summe
else:
faktor = None
for zeit, kw, bis_hier in verlauf:
wh = round(anfang + bis_hier * faktor) if faktor else None
neu.append((zeit, name, round(kw, 3), wh))
print(name, beginn, "bis", ende, "-", len(verlauf), "Punkte,",
round(summe / 1000, 2), "kWh aufsummiert",
("gegen " + str(round((schluss - anfang) / 1000, 2)) + " kWh Zaehler"
if faktor else "(ohne Zaehlerstaende, wh_total bleibt leer)"))
if probe:
print(len(neu), "Zeilen - Probe, nichts geschrieben.")
return
c.execute("DELETE FROM skoda_ladepunkte WHERE quelle = 'energyflow' "
"AND datetime BETWEEN %s AND %s", (von, bis))
entfernt = c.rowcount
c.executemany("INSERT INTO skoda_ladepunkte (datetime, wallbox, kw, wh_total, quelle) "
"VALUES (%s,%s,%s,%s,'energyflow')", neu)
v.commit()
print(len(neu), "Zeilen nachgetragen,", entfernt, "fruehere Nachtraege ersetzt.")
if __name__ == "__main__":
main()