"""Pruefung der Zuordnung in gatherSkodaData.py ohne Fahrzeug und ohne Netz. Das Auto kommt erst naechste Woche. Bis dahin laesst sich wenigstens sicherstellen, dass eine Antwort richtig zerlegt wird und dass die Faelle nicht abstuerzen, die im Betrieb sicher vorkommen: fehlende Teilbereiche, UNKNOWN statt eines Werts, Fahrzeug in Bewegung, leere Antwort. python3 skoda_test.py """ import datetime import time import gatherSkodaData as g fehler = [] def pruefe(was, ist, soll): if ist != soll: fehler.append(was+": "+repr(ist)+" statt "+repr(soll)) def frisch(): g.ret = g.SkodaData() return g.ret # --- 1. Vollstaendige Antwort --------------------------------------------- r = frisch() g.uebernehmen({"vehicle":{ "vin":"TMBJB9NY5RF999999","name":"Elroy", "charging":{"isVehicleInSavedLocation":True, "carCapturedTimestamp":"2026-09-02T05:00:00Z", "status":{"state":"CHARGING","chargeType":"AC","chargePowerInKw":10.8, "battery":{"stateOfChargeInPercent":71, "remainingCruisingRangeInMeters":249000}}, "settings":{"targetStateOfChargeInPercent":80,"chargingCareMode":"ACTIVATED"}}}}) pruefe("SoC", r.soc, 71) pruefe("Reichweite km", r.range_km, 249.0) pruefe("Rohwert bleibt roh", r.range_m, 249000) pruefe("UTC nach Ortszeit", r.capChg, datetime.datetime(2026,9,2,7,0,0)) pruefe("Batterieschonung", r.careMode, True) # --- 2. Reichweite, die schon in Kilometern kommt -------------------------- # Das Feld heisst Meter, das Beispiel der Spec sieht nach Kilometern aus. # Beide Lesarten muessen zur selben Anzeige fuehren. r = frisch() g.uebernehmen({"vehicle":{"charging":{"status":{"battery": {"stateOfChargeInPercent":71,"remainingCruisingRangeInMeters":249}}}}}) pruefe("249 gilt als Kilometer", r.range_km, 249.0) r = frisch() g.uebernehmen({"vehicle":{"charging":{"status":{"battery": {"stateOfChargeInPercent":3,"remainingCruisingRangeInMeters":900}}}}}) pruefe("900 gilt als Kilometer", r.range_km, 900.0) # --- 3. UNKNOWN ist kein Nein --------------------------------------------- r = frisch() g.uebernehmen({"vehicle":{"status":{ "overall":{"locked":"UNKNOWN","doorsLocked":"UNKNOWN","doors":"UNKNOWN", "windows":"UNSUPPORTED","lights":"OFF"}, "detail":{"sunroof":"UNSUPPORTED","trunk":"CLOSED","bonnet":"CLOSED"}}}}) pruefe("UNKNOWN wird None", r.locked, None) r = frisch() g.uebernehmen({"vehicle":{"status":{ "overall":{"locked":"NO","doorsLocked":"NO","doors":"OPEN", "windows":"CLOSED","lights":"OFF"}, "detail":{"sunroof":"CLOSED","trunk":"CLOSED","bonnet":"CLOSED"}}}}) pruefe("NO wird False", r.locked, False) # --- 4. Fehlende Teilbereiche loeschen den letzten Stand nicht ------------- # Die API laesst einen Teil weg, wenn sie ihn gerade nicht bekommt, und legt # stattdessen einen Eintrag in errors ab. Der alte Wert ist dann die bessere # Auskunft als eine Null. r = frisch() g.uebernehmen({"vehicle":{"odometer":{"mileageInKm":12753}, "charging":{"status":{"battery":{"stateOfChargeInPercent":71}}}}}) g.uebernehmen({"vehicle":{"odometer":{"mileageInKm":12753}}, "errors":[{"type":"CHARGING_UNAVAILABLE"}]}) pruefe("SoC bleibt stehen", r.soc, 71) pruefe("Fehler vermerkt", r.apiErrors, "CHARGING_UNAVAILABLE") # --- 5. Fahrzeug in Bewegung: keine Koordinaten --------------------------- r = frisch() g.uebernehmen({"vehicle":{"parkingPosition":{"state":"PARKED", "gpsCoordinates":{"latitude":47.5779944,"longitude":10.2623373}}}}) g.uebernehmen({"vehicle":{"parkingPosition":{"state":"IN_MOTION"}}}) pruefe("Zustand folgt", r.parkState, "IN_MOTION") pruefe("letzte Position bleibt", r.lat, 47.577994) # --- 6. Fahrenheit wird umgerechnet --------------------------------------- r = frisch() g.uebernehmen({"vehicle":{"airConditioning":{"state":"COOLING", "targetTemperature":{"value":72.5,"unit":"FAHRENHEIT"}}}}) pruefe("Fahrenheit nach Celsius", r.acTargetC, 22.5) # --- 7. Leere und kaputte Antworten stuerzen nicht ab --------------------- r = frisch() g.uebernehmen({}) g.uebernehmen({"vehicle":{}}) g.uebernehmen({"vehicle":{"charging":{},"status":{},"odometer":{}, "parkingPosition":{},"airConditioning":{}}}) pruefe("leer bleibt leer", r.soc, 0) # --- 8. Ladeleistung faellt auf 0, wenn das Kabel raus ist ---------------- r = frisch() g.uebernehmen({"vehicle":{"charging":{"status":{"state":"CHARGING","chargePowerInKw":10.8}}}}) g.uebernehmen({"vehicle":{"charging":{"status":{"state":"CONNECT_CABLE"}}}}) pruefe("abgesteckt heisst 0 kW", r.chgKw, 0.0) r = frisch() g.uebernehmen({"vehicle":{"charging":{"status":{"state":"CHARGING","chargePowerInKw":10.8}}}}) g.uebernehmen({"vehicle":{"charging":{"status":{"state":"CONSERVING"}}}}) pruefe("gesteckt behaelt den Wert", r.chgKw, 10.8) # --- 9. Abrufintervall folgt der Lage ------------------------------------ r = frisch() g._st["letzteAend"] = 0.0 g._haus["wbPlug"] = False; g._haus["wbKw"] = 0.0 r.chgState = "CHARGING" pruefe("laedt", g._intervall(), g._I_LADEN) r.chgState = "" g._haus["wbPlug"] = True; g._haus["wbKw"] = 7.4 pruefe("Wallbox liefert", g._intervall(), g._I_LADEN) g._haus["wbKw"] = 0.0 pruefe("gesteckt, laedt nicht", g._intervall(), g._I_GESTECKT) g._haus["wbPlug"] = False r.parkState = "IN_MOTION" pruefe("unterwegs", g._intervall(), g._I_FAHRT) r.parkState = "PARKED" pruefe("lange nichts", g._intervall(), g._I_RUHE) r.error = 3 pruefe("nach Fehlern zurueckhaltend", g._intervall(), 240.0) r.error = 0 # --- 10. Kontingent weitet den Abstand ------------------------------------ r = frisch() pruefe("100 Anfragen in 60 s", round(g._kontingent( {"RateLimit-Remaining":"100","RateLimit-Reset":"60"}),3), 0.72) pruefe("2 Anfragen in 600 s", round(g._kontingent( {"RateLimit-Remaining":"2","RateLimit-Reset":"600"}),1), 360.0) pruefe("nichts mehr frei", g._kontingent( {"RateLimit-Remaining":"0","RateLimit-Reset":"120"}), 125.0) pruefe("ohne Header kein Einfluss", g._kontingent({}), 0.0) # --- 11. Nur echte Aenderungen erzeugen eine Zeile ------------------------ r = frisch() g.uebernehmen({"vehicle":{"charging":{"status":{"battery":{"stateOfChargeInPercent":71}}}}}) a = g._signatur() g._haus["pvKw"] = 8.2 # Hausseite aendert sich staendig b = g._signatur() pruefe("Hausseite loest nichts aus", a, b) g.uebernehmen({"vehicle":{"charging":{"status":{"battery":{"stateOfChargeInPercent":72}}}}}) pruefe("SoC-Sprung loest aus", g._signatur() != a, True) # --- 12. Plug-in-Hybrid: beide Antriebe werden getrennt gefuehrt ---------- # Welcher Skoda kommt, steht noch nicht fest. Ohne include-Parameter liefert # die API von sich aus fuelStatus, wenn ein Verbrenner mitfaehrt - die # Spalten dafuer muessen von Anfang an da sein, nachtragen laesst sich das # nicht. r = frisch() g.uebernehmen({"vehicle":{"fuelStatus":{ "carType":"HYBRID","totalRangeInKm":620, "carCapturedTimestamp":"2026-09-02T05:00:00Z", "primaryEngineRange":{"engineType":"ELECTRIC","currentSoCInPercent":71, "remainingRangeInKm":110}, "secondaryEngineRange":{"engineType":"GASOLINE","currentFuelLevelInPercent":87, "remainingRangeInKm":510}}}}) pruefe("Fahrzeugart", r.carType, "HYBRID") pruefe("Gesamtreichweite", r.totalRangeKm, 620.0) pruefe("Antrieb 1 elektrisch", (r.eng1Type, r.eng1Soc), ("ELECTRIC", 71)) pruefe("Antrieb 2 Benzin", (r.eng2Type, r.eng2FuelPct), ("GASOLINE", 87)) pruefe("Tankanzeige fuer die Oberflaeche", r.fuelPct, 87) pruefe("Zeitstempel Verbrenner", r.capFuel, datetime.datetime(2026,9,2,7,0,0)) # Umgekehrte Reihenfolge - die Spec legt nicht fest, welcher Motor wo steht. r = frisch() g.uebernehmen({"vehicle":{"fuelStatus":{"carType":"HYBRID", "primaryEngineRange":{"engineType":"DIESEL","currentFuelLevelInPercent":42}, "secondaryEngineRange":{"engineType":"ELECTRIC","currentSoCInPercent":66}}}}) pruefe("Tank auch an erster Stelle", r.fuelPct, 42) # Reiner Stromer: kein fuelStatus, alles bleibt leer. r = frisch() g.uebernehmen({"vehicle":{"charging":{"status":{"battery": {"stateOfChargeInPercent":71}}}},"errors":[{"type":"FUEL_STATUS_UNSUPPORTED"}]}) pruefe("Stromer ohne Tank", (r.carType, r.fuelPct), ("", 0)) # --- 13. Das Kontingent wird unter Dauerlast eingehalten ------------------ # Die Doku nennt 20 Anfragen je Stunde und Schluessel. Beim Laden greift das # kuerzeste Intervall - genau dort muss die Sperre halten. Simuliert wird ein # ganzer Tag Dauerladen, ohne echte Zeit und ohne Netz. g._verbrauch.clear() zeitpunkte = [] t = 0.0 while t < 24*3600: zeitpunkte.append(t) g._gezaehlt(t) t += max(g._I_MIN, g._I_LADEN, g._budgetSperre(t)) schlimmste = 0 for i, t0 in enumerate(zeitpunkte): in_fenster = sum(1 for t1 in zeitpunkte[i:] if t1 - t0 < 3600) schlimmste = max(schlimmste, in_fenster) pruefe("nie mehr als das Limit je Stunde", schlimmste <= g._LIMIT, True) pruefe("das eigene Budget wird eingehalten", schlimmste <= g._BUDGET_GETEILT, True) pruefe("und auch ausgenutzt", schlimmste >= g._BUDGET_GETEILT - 3, True) # Mit eigenem Schluessel fuer die Steuerung darf mehr genutzt werden. g._st["eigenerCmdKey"] = True g._verbrauch.clear() for i in range(g._BUDGET_GETEILT): g._gezaehlt(2000.0 + i) pruefe("geteiltes Budget reicht dem Modul allein nicht", g._budgetSperre(2000.0 + g._BUDGET_GETEILT), 0.0) g._verbrauch.clear() for i in range(g._BUDGET_ALLEIN): g._gezaehlt(2000.0 + i) pruefe("eigenes Budget greift bei 18", g._budgetSperre(2000.0 + g._BUDGET_ALLEIN) > 3500, True) g._st["eigenerCmdKey"] = False g._verbrauch.clear() # Ein einzelner Anstoss ausser der Reihe darf das Budget nicht sprengen. g._verbrauch.clear() for i in range(g._BUDGET_GETEILT): g._gezaehlt(1000.0 + i) pruefe("Budget voll: Sperre greift", g._budgetSperre(1000.0+g._BUDGET_GETEILT) > 3500, True) g._verbrauch.clear() pruefe("Budget leer: keine Sperre", g._budgetSperre(0.0), 0.0) # --- 14. Header mit den echten Zahlen ------------------------------------- g.ret = g.SkodaData() pruefe("19 Anfragen in 3400 s", round(g._kontingent( {"RateLimit-Remaining":"19","RateLimit-Reset":"3400"}),0), 215.0) pruefe("Restkontingent gemerkt", g.ret.rlRemaining, 19) # --- 15. Ladebeginn und Ladeende stossen einen Abruf an ------------------- # Bei 20 Anfragen je Stunde sind das die beiden wertvollsten Augenblicke: # der Ladestand davor und danach traegt die Kapazitaetsrechnung. import asyncio g._st["ladenVorher"] = False g._st["naechster"] = time.time() + 9999 asyncio.get_event_loop().run_until_complete(g.gatherData(wbKw=10.8, wbPlug=True)) pruefe("Beginn wird bemerkt", g._st["ladenVorher"], True) pruefe("Abruf vorgezogen", g._st["naechster"] < time.time() + 120, True) g._st["naechster"] = time.time() + 9999 asyncio.get_event_loop().run_until_complete(g.gatherData(wbKw=0.0, wbPlug=True)) pruefe("Ende wird bemerkt", g._st["ladenVorher"], False) pruefe("Abruf wieder vorgezogen", g._st["naechster"] < time.time() + 120, True) g._st["naechster"] = time.time() + 9999 asyncio.get_event_loop().run_until_complete(g.gatherData(wbKw=0.0, wbPlug=True)) pruefe("ohne Flanke kein Anstoss", g._st["naechster"] > time.time() + 1000, True) print() if fehler: for f in fehler: print("FEHLER "+f) print("\n"+str(len(fehler))+" von "+str(len(fehler))+" Pruefungen fehlgeschlagen.") else: print("Alle Pruefungen bestanden.")