Jak za 1150 zł zrobiłem inteligentne ładowanie auta elektrycznego z nadwyżek słońca
Tomasz Kwietniewski · · 47 min czytania

Mam fotowoltaikę 9 kWp, magazyn energii 15 kWh i Citroëna Spacetourer elektrycznego z baterią 75 kWh. Przez chwilę ładowałem auto „na ślepo” — podłączałem kabel i tyle. Tymczasem latem moje panele produkują więcej prądu niż potrzebuję, a nadwyżki szły do sieci. Postanowiłem to zmienić.
Problem: moja ładowarka — dé EV Charger 11 kW z Wi-Fi za około 1150 zł — teoretycznie nie oferuje żadnych zaawansowanych konfiguracji sterowania mocą. Łączy się z chmurą Tuya przez aplikację Smart Life i tyle. Nie ma API, nie ma integracji z Home Assistant, nie ma możliwości ustawienia „ładuj tylko z nadwyżek”.
A jednak udało się to osiągnąć. Oto jak — i na co uważać.
Co mam w domu
- Fotowoltaika: 9 kWp (18 paneli JA Solar n-type)
- Falownik hybrydowy: Sofar HYD 8KTL-3PH
- Magazyn energii: Sofar BTS E15-DS5 (15 kWh)
- Auto: Citroën Spacetourer Electric 75 kWh
- Ładowarka: dé EV Charger 11 kW, 3-fazowa, Wi-Fi, protokół Tuya
- Centrum automatyki: Home Assistant na Synology NAS DS420+
- Taryfa: Pstryk (dynamiczne ceny energii)
Dlaczego Local Tuya nie zadziałało — protokół 3.5
Zanim doszedłem do rozwiązania z TinyTuya, próbowałem najprostszej drogi: integracji Local Tuya dostępnej przez HACS. To popularna integracja która pozwala sterować urządzeniami Tuya lokalnie bezpośrednio z Home Assistant — bez żadnego kodowania w Pythonie.
Przeprowadziłem szczegółową diagnostykę. Połączenie TCP z ładowarką na porcie 6668 działało prawidłowo, Local Key był poprawny, adres IP również. Problem tkwił gdzie indziej — dwa czynniki jednocześnie:
Problem 1: Protokół 3.5
Ładowarka dé EV używa protokołu Tuya w wersji 3.5, natomiast integracja Local Tuya obsługuje tylko wersje do 3.4. To powodowało że wszystkie encje pozostawały w stanie unavailable mimo prawidłowej konfiguracji.
Problem 2: Brak UDP discovery
Ładowarka nie wysyłała broadcastu UDP discovery, którego Local Tuya oczekuje do automatycznego wykrycia urządzenia w sieci. Nawet ręczne wpisanie IP nie pomagało — integracja nie mogła nawiązać poprawnej sesji.
Rozwiązanie: AppDaemon + TinyTuya
Biblioteka TinyTuya obsługuje protokół 3.5 i połączyła się bez problemów. Uruchomiłem ją przez add-on AppDaemon w Home Assistant, który pozwala instalować paczki Pythona trwale.
Oficjalne nazwy Data Pointów pobrane przez Tuya IoT Platform:
| DP | Nazwa w API | Znaczenie |
|---|---|---|
| 101 | x_work_state | Stan pracy jako liczba |
| 102 | x_metrics | Dane pomiarowe JSON (napięcie/prąd/moc per faza) |
| 109 | x_work_st_debug | Status: WORKING / SLEEP / IDLE / PAUSE |
| 140 | x_do_charge | Start/Stop ładowania (bool) |
| 150 | x_charge_current | Prąd ładowania w amperach |
Protokół 3.5 — to kluczowa informacja dla każdego kto będzie chciał zintegrować tę ładowarkę z Home Assistant. Local Tuya nie zadziała. Jedyna działająca droga to AppDaemon + TinyTuya.
Jeśli kiedyś Local Tuya doda obsługę protokołu 3.5, konfiguracja stanie się znacznie prostsza — wystarczy dodać urządzenie przez UI bez pisania kodu.
Kluczowe odkrycie: TinyTuya i lokalna kontrola
Urządzenia Tuya (Smart Life) domyślnie komunikują się przez chmurę producenta. Każde kliknięcie w aplikacji wędruje przez serwery Tuya i wraca do urządzenia. To oznacza zależność od internetu, opóźnienia i — co ważne — limity zapytań w darmowym planie API (około 1000 dziennie).
Dla użytkowników z Polski dane trafiają na serwer w Frankfurcie (AWS, Niemcy) — nie w Chinach jak można by się spodziewać. Tuya ma centra danych w Europie Centralnej i Zachodniej obsługujące europejskich użytkowników. Opóźnienia są więc minimalne, ale limit zapytań pozostaje problemem.
Ale jest sposób, żeby to obejść. Biblioteka TinyTuya pozwala komunikować się z urządzeniem bezpośrednio po sieci lokalnej, bez udziału chmury. Wystarczy znać trzy rzeczy:
- Device ID — unikalny identyfikator urządzenia
- Local Key — klucz szyfrujący (pobierany jednorazowo z chmury Tuya)
- IP urządzenia — lokalny adres w sieci domowej
Po jednorazowym pobraniu klucza z chmury (przez Tuya IoT Platform), całe sterowanie odbywa się lokalnie. Zero limitów, zero opóźnień, zero zależności od internetu.
import tinytuya
d = tinytuya.Device(
dev_id="TWOJ_DEVICE_ID",
address="192.168.X.X",
local_key="TWOJ_LOCAL_KEY",
version=3.5
)
status = d.status()
print(status)
Data Pointy — jak ładowarka mówi o sobie
Urządzenia Tuya komunikują się przez tzw. Data Pointy (DP) — numerowane kanały danych. Każde urządzenie ma swój zestaw DP. Żeby dowiedzieć się co DP znaczą, trzeba zapytać urządzenie i przeanalizować odpowiedź w różnych stanach pracy.
Kluczowa pułapka: klucze w słowniku dps są stringami, nie integerami. Dlatego dps.get(109) zawsze zwróci None — trzeba używać dps.get("109"). To jeden z tych błędów który potrafi zająć godzinę debugowania.
Pełna mapa Data Pointów
Po dogłębnej analizie udało mi się rozszyfrować wszystkie DP tej ładowarki:
| DP | Typ | Znaczenie | Przydatność |
|---|---|---|---|
| 102 | JSON | Dane pomiarowe per faza (napięcie, prąd, moc) | ⭐⭐⭐ używam |
| 105 | JSON | Historia ostatniej sesji (start, koniec, czas, prąd) | ⭐⭐⭐ bardzo przydatne |
| 106 | JSON | Dane techniczne (wersja firmware, parametry) | ⭐ informacyjne |
| 107 | string | Lista dostępnych poziomów prądu:[6, 8, 10, 13, 16] | ⭐⭐ warto znać |
| 109 | string | Status:WORKING / SLEEP / IDLE / PAUSE | ⭐⭐⭐ używam |
| 140 | bool | Start/Stop ładowania | ⭐⭐⭐ używam |
| 150 | int | Prąd ładowania w A (6–16) | ⭐⭐⭐ używam |
| 151 | JSON | Wbudowany harmonogram ładowania | ⭐⭐ alternatywa dla AppDaemon |
| 152 | int | Maksymalny prąd (16A) | ⭐ informacyjne |
| 155 | bool | Nieznane — prawdopodobnie blokada ładowania | ❓ do zbadania |
| 156 | bool | Nieznane — może tryb jednofazowy/trójfazowy | ❓ do zbadania |
| 157 | int | Nieznane (zawsze 1) | ❓ do zbadania |
| 188 | bool | Nieznane — może lock kabla | ❓ do zbadania |
DP 105 — historia sesji gotowa do odczytu
Ten DP zawiera dane o ostatnim ładowaniu bez potrzeby własnych liczników:
{
"t": "2026-04-30 17:13:26",
"s": "17:13",
"e": "17:41",
"d": 1677,
"c": 17
}
Gdzie s = godzina startu, e = godzina końca, d = czas trwania w sekundach (1677s ≈ 28 min), c = prąd w A.
DP 102 — dane pomiarowe i ukryta skala
Format danych pomiarowych z DP 102:
{
"L1": [2260, 144, 32],
"L2": [2260, 147, 33],
"L3": [2260, 145, 32],
"p": 98,
"e": 11
}
Gdzie L1[2], L2[2], L3[2] to moc per faza, a p to łączna moc — mnożona przez 100 (98 × 100 = 9800 W = 9,8 kW). Uwaga: nigdzie w dokumentacji tego nie ma — odkryłem to porównując wartości z aplikacją Smart Life.
DP 151 — wbudowany harmonogram
Ładowarka ma własny harmonogram który można programować:
{"m": 0, "dt": 0, "ss": "15:00", "se": "17:00"}
W prostszych przypadkach (np. „ładuj zawsze w nocy 23:00–6:00”) można ustawić harmonogram bezpośrednio bez AppDaemon. Do dynamicznego sterowania zależnego od cen i PV — AppDaemon jest niezastąpiony.
Logika sterowania — sześć trybów
Skrypt AppDaemon co 30 sekund sprawdza stan instalacji i podejmuje decyzję. W aktualnej wersji obsługuje sześć trybów pracy:
| Tryb | Warunek | Działanie |
|---|---|---|
EMERGENCY | Włączony ręcznie przez toggle w HA | Ładuj natychmiast na 13A (~9 kW), niezależnie od PV i cen |
NEGATIVE_PRICE | Cena Pstryk < 0 zł/kWh | Ładuj na 13A (~9 kW, bufor ~2 kW na dom) |
WINTER_NIGHT | Tryb zimowy włączony, godz. 22–6 | Ładuj na 10A (tania taryfa nocna) |
SOLAR | SOC baterii ≥ 95% i nadwyżka ≥ 1,6 kW | Ładuj proporcjonalnie do nadwyżki (6–16A) |
BATTERY_PRIORITY | SOC < 95% | Czekaj, priorytet ładowania baterii |
IDLE | Brak nadwyżek lub auto niepodłączone | Ładowarka wyłączona |
Tryby sprawdzane są w kolejności od góry — EMERGENCY ma najwyższy priorytet.
Tryb EMERGENCY — ładowanie awaryjne na maksa
Dodany po tym jak pewnego dnia wróciłem do domu z prawie pustą baterią auta i za godzinę musiałem jechać znowu. Słońca było mało, a skrypt solarny czekał na nadwyżki.
Rozwiązanie: przełącznik w dashboardzie HA z timerem. Ustawiasz ile godzin (0,5–8h), włączasz toggle — ładowarka rusza natychmiast na 13A (~9 kW). Nie czeka na słońce, może drenować magazyn (ale zatrzyma się gdy SOC baterii spadnie poniżej 20%). Po upływie czasu automatycznie wraca do trybu normalnego.
EMERGENCY_CURRENT_A = 13 # zostawia ~2 kW bufora na dom przy przyłączu 11 kW
SOC_EMERGENCY_MIN = 20 # nie drenuj magazynu poniżej 20%
Znak PCC Sofara — weryfikuj empirycznie
To jedna z ważniejszych pułapek. Sensor sensor.sofar_modbus_inverter_active_power_pcc_total może mieć różny znak w zależności od wersji firmware i trybu pracy falownika. W mojej instalacji:
- Dodatni PCC = eksport do sieci (nadwyżka)
- Ujemny PCC = import z sieci (brak nadwyżki)
Sprawdź w Developer Tools wartość tego sensora gdy wiesz że eksportujesz (bateria pełna, słońce świeci). Jeśli wartość jest ujemna przy eksporcie — zamień znak w kodzie.
Nadwyżka to nie samo PCC
Naturalny odruch: „nadwyżka to jest to, co wypycham do sieci”, czyli PCC. Przy falowniku hybrydowym to jednak zły sygnał sterujący, bo Sofar w trybie self-use aktywnie kompensuje deficyt z magazynu i trzyma PCC blisko zera. Przy PV 1 kW, domu 5 kW i ładowarce ciągnącej 4 kW licznik pokaże PCC bliskie zeru, a skrypt uzna, że jest równowaga. Realnie w tym czasie opróżnia baterię domową.
Dlatego nadwyżkę liczę jako minimum z dwóch sygnałów:
surplus_without_ev_kw = min(grid_power, pv_power - load_power)
PV minus dom widzi deficyt maskowany przez magazyn. PCC z kolei pilnuje, żeby nie zabrać mocy, którą falownik akurat wpompowuje do baterii (przy SOC 95-99% PV minus dom jest większe niż realny eksport). Minimum z obu to nadwyżka, którą można wziąć bez szkody dla magazynu.
Sprawdzone na żywych danych w dniu wdrożenia: PV 2,8 kW, dom 0,4 kW, a PCC tylko 0,04 kW. Różnica 2,4 kW szła do ładującego się magazynu. Sam PV minus dom uznałby ją za wolną i podebrał baterii, min() widzi realne 40 W.
Uśrednianie, czyli eliminacja migotania
Sygnał „migocze”: raz -0,1 kW, raz +0,2 kW, raz -0,5 kW, nawet gdy bilans jest w zasadzie zero. To normalne przy hybrydowym falowniku, regulacja nie jest idealna. Bez filtrowania skrypt zmieniałby prąd ładowania co 30 sekund.
Rozwiązanie: uśrednianie z ostatnich 3 odczytów (90 sekund):
PCC_HISTORY_SIZE = 3
# uwaga: do historii trafia wartość PO doliczeniu poboru ładowarki,
# żeby każda próbka znaczyła to samo (patrz Problem 19)
self._surplus_history.append(available_kw)
if len(self._surplus_history) > PCC_HISTORY_SIZE:
self._surplus_history.pop(0)
avg_available_kw = sum(self._surplus_history) / len(self._surplus_history)
Bias +1000W — agresywne wykorzystanie nadwyżek
Prąd ładowarki zmienia się skokowo co 690W (1A × 3 fazy × 230V). Żeby skrypt był trochę bardziej „agresywny” i częściej wybierał wyższy prąd, dodałem stały bias +1000W do obliczonej nadwyżki. Dzięki temu auto startuje już przy ~0,6 kW realnego eksportu zamiast czekać na pełne 1,6 kW. W kodzie bias jest wydzielony jako nazwana stała:
SURPLUS_BIAS_W = 1000 # bufor zachęcający do startu
# Bez podłogi: przy imporcie wychodzi ujemne i regulacja redukuje prąd
surplus_w = avg_available_kw * 1000 + SURPLUS_BIAS_W
Ważne, żeby nie zastępować ujemnej nadwyżki stałą wartością biasu. Pierwotna wersja tak robiła i sterowanie „uciekało” w górę przy zachmurzeniu. Szczegóły w Problemie 19.
Przy cenie 0,15 zł/kWh to koszt ~15 groszy za godzinę ładowania w zamian za lepsze wykorzystanie słońca. Latem przy cenach bliskich zeru — bez znaczenia.
Stan PAUSE — ładowarka gotowa ale wstrzymana
Gdy auto jest podłączone ale ładowanie jest wstrzymane (np. przez harmonogram), ładowarka raportuje stan PAUSE. Stary kod nie obsługiwał tego stanu i nie wysyłał START — auto stało podłączone ale się nie ładowało.
Rozwiązanie: traktuj PAUSE jak IDLE — auto jest gotowe do ładowania:
CHARGER_READY_STATES = {"PAUSE", "SLEEP", "IDLE", "UNKNOWN"}
CHARGER_WORKING_STATES = {"WORKING"}
Pułapki techniczne — kompletna lista
Problem 1: Protokół Tuya 3.5
Local Tuya obsługuje tylko do wersji 3.4. Jedyne rozwiązanie: AppDaemon + TinyTuya.
Problem 2: Klucze DP jako stringi
dps.get("109") # poprawnie
dps.get(109) # zawsze None
Problem 3: DP 151 blokuje START
Ładowarka ma wbudowany harmonogram (DP 151). Gdy harmonogram jest aktywny, ładowarka ignoruje zewnętrzne komendy START i pozostaje w PAUSE. Rozwiązanie — wyczyść harmonogram przy każdym starcie:
self._device.set_value("151", json.dumps({"m":0,"dt":0,"ss":"00:00","se":"00:00"}))
Problem 4: Znak PCC zmienia się po zmianie trybu Sofara
Po zmianie trybu falownika (np. z Self-use na Time of Use) znak PCC może się odwrócić. Zawsze weryfikuj empirycznie po każdej zmianie konfiguracji falownika.
Problem 5: Moc DP 102 mnożona x100
L1[2], L2[2], L3[2] to moc per faza w jednostkach x100W. Wartość 32 oznacza 3200W, nie 32W.
Problem 6: Stan PAUSE ignorowany
Gdy auto podłączone ale harmonogram wstrzymał ładowanie — ładowarka raportuje PAUSE. Stary kod nie wysyłał START w tym stanie.
Problem 7: Uśrednianie PCC konieczne
Bez filtrowania migające wartości PCC powodują chaotyczne zmiany prądu co 30 sekund.
Problem 8: Próg startu za wysoki
Pierwotny próg START_SURPLUS_W = 5000W był za wysoki — system nie startował przy nadwyżkach 3–4 kW. Aktualny próg: 1600W (razem z biasem 1000W to znaczy, że auto startuje już przy ~0,6 kW realnego eksportu PCC).
Problem 9: Serwery Tuya dla Polski
Dla europejskich użytkowników dane trafiają na serwer w Frankfurcie (AWS). Nie w Chinach. To ważne przy konfiguracji Tuya IoT Platform — wybierz region „Central Europe”.
Problem 10: Helpery tylko przez UI
Encje zdefiniowane w YAML są read-only dla serwisów HA. Twórz helpery wyłącznie przez UI (Settings → Helpers → Add).
Problem 11: Nie twórz sensorów przez AppDaemon set_state()
W HA 2026.x API odrzuca encje z atrybutami unit_of_measurement i device_class tworzonymi przez AppDaemon. Używaj input_text jako pośrednika i template sensorów w configuration.yaml.
Problem 12: AppDaemon skanuje folder apps/ rekurencyjnie
AppDaemon ładuje wszystkie pliki .yaml z folderu apps/ — łącznie z podfolderami. Jeśli wewnątrz apps/ umieścisz backup z poprzednim apps.yaml, AppDaemon załaduje go razem z aktualnym i uruchomi duplikaty wszystkich aplikacji.
W praktyce wygląda to tak: masz jeden skrypt sterujący ładowarką, a działają dwie instancje — każda wysyła komendy do ładowarki co 30 sekund, wzajemnie sobie przeszkadzając. W logach zobaczysz dwa razy Calling initialize() for ev_charger_* przy starcie.
Rozwiązanie: trzymaj backupy poza folderem apps/, np. w addon_configs/a0d7b954_appdaemon/_backups/.
Problem 13: STOP-spam w gałęzi IDLE
W _apply_decision gałąź BATTERY_PRIORITY/IDLE/OFFLINE początkowo nie miała guardu sprawdzającego ostatnio wysłany switch — co iterację (30 s) wysyłała komendę STOP do ładowarki, nawet jeśli już wcześniej została wysłana. Skutek dwojaki: niepotrzebne pakiety przez sieć do wallboxa oraz słyszalne klikanie stycznika ładowarki — każdy STOP wymusza cykl przekaźnika.
W logach widać było po kilkanaście STOPów pod rząd w 8 minutach mimo, że stan logiczny się nie zmieniał.
Rozwiązanie: dodać guard if self._last_sent_switch != False: analogicznie do gałęzi SOLAR (gdzie analogiczny guard != True już był). Dzięki temu STOP idzie raz na przejście z aktywnego trybu w IDLE, a nie co iterację.
Problem 14: DP 102 potrafi się „zamrozić” — firmware quirk dé EV v2.9.4
Najbardziej podstępny problem jaki spotkałem. Firmware wallboxa dé EV (sprawdzane na wersji 2.9.4) potrafi zamrozić cały blok pomiarów w DP 102 — wartości napięć, prądów, mocy, energii sesji i temperatury (L1/L2/L3/p/e/t) zwracają identyczny string przez wiele godzin, mimo że auto fizycznie się ładuje. DP 109 (status) nadal raportuje poprawnie WORKING, sterowanie (DP 140 switch, DP 150 current) działa, tylko pomiary kłamią.
Diagnoza tego trwała godziny. Wprowadzało w błąd, że Smart Life cyklicznie pokazywało „Charging”/„Paused” — sugerując że to wallbox cyklicznie zatrzymuje sesję. W rzeczywistości auto ładowało stabilnie ~8 kW, tylko nasz skrypt liczył 0 W (a więc też 0 kWh do liczników miesięcznych).
Rozstrzygnięciem był pełny bilans Sofara — porównanie PV, obciążenia, magazynu i PCC — które jednoznacznie pokazało że ~8 kW znika gdzieś (czyli idzie do auta).
Lekarstwo: w aplikacji Smart Life otworzyć urządzenie → Settings (zębatka) → Reboot (NIE Reset to Factory — to kasuje pairing). Po ~30 sekundach DP 102 wraca do raportowania prawdziwych wartości. Mechanizm — najpewniej zerwany sync wallboxa z chmurą Tuya, soft reboot przywraca.
W skrypcie dodałem watchdog który ostrzega w logach (poziom WARNING) gdy w aktywnym trybie ładowania utrzymuje się status=WORKING + power=0W przez ponad 10 minut. Próg konfigurowalny przez stałą WATCHDOG_FROZEN_DP_THRESHOLD. Skrypt nie restartuje sam — wymagana ręczna interwencja w Smart Life (na razie, do dalszego rozważenia).
Problem 15: DP 151 — chmura Tuya potrafi wpychać harmonogram
Po reboocie wallboxa zaobserwowałem że DP 151 (harmonogram) zmienił się z pustego {"m":0,"dt":0,"ss":"00:00","se":"00:00"} na {"m":0,"dt":0,"ss":"15:00","se":"17:00"} — chmura Tuya wpchnęła resztkowy harmonogram. Pole m:0 oznacza nieaktywny, więc w tym przypadku nie blokuje ładowania, ale daje ślad że chmura może modyfikować wallbox lokalnie bez naszego udziału.
W skrypcie dodałem diagnostykę logującą każdą zmianę DP 151 oraz utrzymuję wywołanie _clear_schedule() w initialize() AppDaemona (raz po starcie skryptu) i w momencie każdego startu sesji. To zabezpiecza przed sytuacją gdy chmura wpchnie tym razem m:1 (aktywny harmonogram blokujący).
Problem 16: DP 102 ma ukryte pole e — energia sesji × 0,1 kWh
Przy okazji diagnostyki Problemu 14 odkryłem że DP 102 oprócz L1/L2/L3/p/t zawiera też pole e — licznik energii bieżącej sesji w jednostce 0,1 kWh. Po e:5 minęło 0,5 kWh sesji, przy e:23 mamy 2,3 kWh. Niezależne od naszego liczenia power_w × dt, mniej podatne na błędy zaokrąglenia.
Plus pole d w DP 102 to duration sesji ale w jakichś własnych jednostkach wallboxa (nie sekundach realnych — przyrosty są nieregularne). Pole t to temperatura ładowarki × 10 (360 = 36,0 °C).
DP 105 z kolei to historia ostatniej zakończonej sesji — JSON z polami t (timestamp), s/e (start/end HH:MM), d (duration w sekundach), c (kWh × 10). Dostępne natychmiast po zakończeniu sesji — można nasłuchiwać zmian DP 105 i mieć w HA dokładny licznik sesji niezależny od power × dt.
Problem 17: Archiwum historii miesięcznej — dane ginęły przy resecie
Licznik _month_energy_kwh oraz utility_meters zerują się 1. dnia miesiąca. Stary kod logował tylko Nowy miesiac! Reset: X kWh i kasował wartość — historia poprzednich miesięcy przepadała, nie dało się porównać miesiąca do miesiąca.
Rozwiązanie ma trzy subtelności warte zapamiętania:
- Wyścig z resetem utility_meter. Gdyby przy przełomie miesiąca odczytać
sensor.produkcja_pv_miesiac„na bieżąco”, można trafić już po jego wyzerowaniu i zapisać ~0. Dlatego skrypt w każdej iteracji zapamiętuje snapshot liczników (_um_snapshot), a przy przełomie archiwizuje snapshot z poprzedniej iteracji — czyli stan na koniec starego miesiąca. Niezależnie od kolejności resetów. - Fallback po restarcie. Jeśli AppDaemon wstanie świeżo (pusty snapshot) tuż po przełomie, sięga po atrybut
last_periodliczników utility_meter — HA trzyma tam wartość poprzedniego cyklu. - Trwały znacznik miesiąca. Zamiast
datetime.now().monthw RAM, miesiąc trzymany jest jakoev_last_ym("YYYY-MM") w pliku persistent. Dzięki temu archiwizacja zadziała nawet, gdy serwer był wyłączony 1. dnia miesiąca i wstał np. 2-go.
Publikacja sensora to osobny temat — patrz Problem 18.
Problem 18: set_state() na sensor.* zwraca 400 w HA 2026.x — publikacja przez REST API
Pierwsza wersja archiwum publikowała sensor.ev_historia_miesieczna przez AppDaemon set_state(). Na HA 2026.6.1 + AppDaemon 4.5.13 kończyło się to w logach błędem:
ERROR HASS: [400] HTTP POST: Bad Request {'attributes': {'friendly_name': ..., 'months': []}}
ERROR HASS: Error setting state: Bad Request
To rozwinięcie Problemu 11. Wbrew pierwotnej hipotezie nie chodzi tylko o unit_of_measurement/device_class — set_state() na encji sensor.* zwraca 400 nawet z gołymi atrybutami. Diagnostyka empiryczna (POST wprost do REST API rdzenia przez proxy supervisora, http://supervisor/core/api/states/... z $SUPERVISOR_TOKEN) pokazała, że samo REST API przyjmuje identyczny payload bez zająknięcia (HTTP 201) — ze stanem int i string, z months, friendly_name, icon. Wina leży więc po stronie ścieżki set_state() w tej wersji AppDaemona, nie HA.
Dlaczego nie input_text + template (sprawdzony wzorzec z Problemu 11)? Bo archiwum (do 120 miesięcy / 10 lat × 7 pól) nie zmieści się w input_text (limit 255 znaków) ani w stanie encji (też 255). Atrybuty encji limitu nie mają.
Rozwiązanie: _publish_history() robi bezpośredni requests.post(...) do REST API rdzenia z tokenem z os.environ["SUPERVISOR_TOKEN"] (addon ma homeassistant_api: true, więc token i proxy są dostępne). Całe archiwum siedzi w atrybucie months; stan = kWh ostatniego miesiąca; jednostki opisują karty dashboardu. Źródłem prawdy pozostaje plik ev_charger_data.json (klucz ev_history) — encja to tylko warstwa prezentacji, odtwarzana przy każdym initialize() (czyli też po restarcie HA, gdy AppDaemon przełącza połączenie).
Problem 19: Regulacja SOLAR „uciekała w górę” przy zachmurzeniu
Najpoważniejszy błąd, jaki znalazłem w tym projekcie, i jednocześnie taki, którego nigdy nie zdiagnozowałem z logów. Objawiał się tylko jako „auto ładuje się mocniej niż powinno, a potem nagle stop”.
Pierwotny kod liczył nadwyżkę tak: jeśli PCC pokazuje eksport, to nadwyżka = PCC + 1000 W, a jeśli import, to po prostu nadwyżka = 1000 W. Ta druga gałąź wyrzucała informację o tym, jak duży jest import. A w trybie SOLAR do nadwyżki dolicza się jeszcze moc ładowarki (bo jej pobór siedzi już w zużyciu domu), więc przy deficycie wychodziło:
available = 1000 W + moc_ladowarki
target = int(available / 690)
To jest dodatnie sprzężenie zwrotne. Ładowarka na 6A (4,1 kW) daje target = int(5140/690) = 7A, przy 7A (4,8 kW) wychodzi 8A, i tak dalej aż do 16A. Wszystko to mimo że słońce zaszło za chmurę, a energia leci z magazynu domowego. Warunek STOP wymagałby przy tym mocy ładowarki poniżej 200 W, czyli w praktyce nie zadziała nigdy. Ładowanie kończyło się dopiero, gdy SOC magazynu spadł poniżej 95% i wszedł BATTERY_PRIORITY, czyli po niepotrzebnym cyklu rozładowania baterii.
Poprawka to nadwyżka liczona jako min(PCC, PV minus dom) bez podłogi (opisana wyżej, w sekcji o nadwyżce). Przy imporcie wartość schodzi poniżej zera, więc regulacja realnie redukuje prąd, a histereza STOP wreszcie działa.
Kolejność operacji okazała się równie ważna, co sam wzór. Pierwsza wersja poprawki uśredniała nadwyżkę, a moc ładowarki dodawała dopiero przy podejmowaniu decyzji. Symulacja pokazała, że to wciąż daje skok prądu: w pierwszej iteracji po starcie sesji historia zawiera próbkę zmierzoną przy wyłączonej ładowarce i próbkę zmierzoną przy 7,6 kW poboru. Dodanie do takiej średniej bieżącej mocy ładowarki liczy ten pobór półtora raza i przy PV 8 kW skrypt skakał na 16A, czyli 11 kW. Kompensacja musi iść przed uśrednianiem, żeby każda próbka w historii znaczyła to samo: ile w tej chwili jest do dyspozycji dla auta.
Jest jeszcze przypadek brzegowy na styku z Problemem 14. Gdy DP 102 zamarznie, ładowarka raportuje WORKING i 0 W, więc kompensacja wychodzi zerowa, a PV minus dom pokazuje ogromny deficyt, bo auto realnie ciągnie 8 kW. Skrypt uznałby to za brak nadwyżki i przerwał realnie trwającą sesję, zamiast pozwolić watchdogowi dojść do progu i ostrzec. Dlatego po dwóch iteracjach WORKING + 0 W do kompensacji podstawiana jest ostatnia znana moc. Dwie iteracje, bo tuż po starcie sesji chwilowe zero jest normalne, auto negocjuje z wallboxem.
Problem 20: Dedup komend bez ponowień to zakleszczenie sterowania
Naprawa STOP-spamu z Problemu 13 wprowadziła pole _last_sent_switch, czyli „nie wysyłaj drugi raz tego samego”. Rozwiązała klikanie stycznika, ale wprowadziła cichą regresję: funkcja wysyłająca komendę łykała wyjątek sieciowy, a _last_sent_switch i tak zapisywało się na „wysłano START”. Jeden zgubiony pakiet w Wi-Fi (a wallbox stoi w garażu, zasięg bywa marny) i skrypt do końca życia procesu uważał, że START poszedł. Ładowanie nie ruszało aż do restartu AppDaemona albo przełączenia trybu awaryjnego. W drugą stronę było gorzej: nieudany STOP oznaczał ładowanie mimo trybu priorytetu baterii.
Poprawka ma dwie warstwy. Po pierwsze, funkcje wysyłające zwracają informację o powodzeniu, a stan „ostatnio wysłane” aktualizuje się tylko przy udanej wysyłce, więc nieudana komenda jest ponawiana w następnej iteracji. Po drugie, osobno obsłużony jest przypadek „komenda poszła, ale wallbox jej nie wykonał”: licznik niezgodności między intencją a stanem ładowarki, ponowienie co 4 iteracje (2 minuty). START ma limit 3 ponowień, bo auto może być po prostu naładowane do 100% i nie przyjmie sesji. STOP dostaje 5 prób i głośny błąd w logach, zamiast walić w stycznik w nieskończoność.
Ten błąd potwierdził się w terenie dokładnie w chwili wdrożenia poprawki. Auto ładowało się wtedy na 5,2 kW przy SOC magazynu 46%, czyli w warunkach, w których stary skrypt powinien był wysłać STOP. Nie wysyłał, bo miał zapamiętane, że już zatrzymał. Ręczny start ze Smart Life przechodził w ten sposób niezauważony. Nowy kod zatrzymał sesję w pierwszej iteracji po restarcie.
Problem 21: TinyTuya zwraca błąd jako słownik, nie wyjątek
device.status() przy problemach sieciowych często nie rzuca wyjątku, tylko zwraca słownik {"Error": "Network Error", "Err": "901"}. Stary kod sięgał po klucz dps, dostawał pusty słownik, status wychodził UNKNOWN, a UNKNOWN jest na liście stanów „gotowy do ładowania”. Efekt: ładowarka uznana za dostępną i gotową, komendy wysyłane w próżnię.
Poprawka sprawdza obecność klucza Error w odpowiedzi i traktuje taką sytuację jak brak łączności. Przy okazji limit ponowień gniazda zszedł z 3 na 1, bo trzy próby po 6 sekund timeoutu plus drugi odczyt potrafiły zablokować wątek AppDaemona na około 36 sekund, czyli dłużej niż interwał pętli.
Problem 22: Nieatomowy zapis pliku z licznikami
ev_charger_data.json trzyma liczniki energii i całe 10-letnie archiwum, a zapisywany był w miejscu: otwórz do zapisu, zrzuć JSON. Przerwanie w trakcie (restart dodatku, brak miejsca) zostawia obcięty plik. Gorzej: od tego momentu każdy kolejny zapis padał już na etapie wczytania starej zawartości, a odczyt cicho zwracał wartości domyślne. Liczniki wyzerowane, archiwum niedostępne, w logach tylko ostrzeżenie co 30 sekund.
Poprawka: zapis do pliku tymczasowego i podmiana przez os.replace(), która jest atomowa w obrębie tego samego systemu plików. Nieczytelny plik jest odkładany z końcówką .corrupt, a skrypt startuje od pustego stanu, zamiast zapętlać się na błędzie.
Problem 23: Regulacja goniąca szum, czyli ładowarka pikająca co 30 sekund
Ten problem zgłosiło ucho, nie log. Dzień po naprawie regulacji usłyszałem przez otwarte okno, że wallbox pika bardzo często. Logi potwierdziły od ręki: w ciągu trzech minut prąd zmienił się pięć razy, w tym sekwencja 10A, 11A, z powrotem 10A w ciągu minuty.
Pierwsza przyczyna to brak strefy nieczułości. Komenda szła do ładowarki, ilekroć nowy cel różnił się od poprzedniego choćby o jeden amper. Tyle że jeden amper to zaledwie 690 W (trzy fazy razy 230 V), a dzielenie jest obcinane w dół. Gdy nadwyżka stanęła dokładnie na granicy stopnia, wystarczyło wahanie rzędu 30 W, czyli ułamka procenta, żeby cel przeskakiwał w każdej iteracji. W symulacji stabilnego słońca z takim właśnie szumem stary kod wygenerował 29 zmian prądu w kwadrans.
Druga przyczyna jest ciekawsza. Porównałem cel z rzeczywistą mocą i okazało się, że auto dochodzi do zadanego prądu z opóźnieniem około minuty: przy celu 10A moc odpowiadała najpierw 8,5A, minutę później 9,4A. A ponieważ zmierzony pobór ładowarki wraca do wyliczenia nadwyżki, sterownik reagował na stan, który jeszcze się nie ustalił, i sam sobie produkował oscylacje. Podręcznikowy błąd: pętla regulacji szybsza niż obiekt, którym steruje.
Rozwiązanie ma dwie warstwy. Histereza plus minus 250 W wokół progu stopnia - żeby podnieść prąd, nadwyżka musi przekroczyć próg z zapasem, i tak samo w drugą stronę. Oraz potwierdzenie zmiany w czasie - nowy cel musi utrzymać się przez dwie iteracje, zanim komenda pójdzie do wallboxa. Wyjątkiem jest spadek o trzy ampery lub więcej, który idzie natychmiast, bo chroni przyłącze 11 kW, gdy nagle ruszy pompa ciepła albo piekarnik.
Świadomie wygładzam też małe redukcje, choć pierwszy szkic poprawki miał je wykonywać od ręki. Zmieniłem zdanie po prostej refleksji: krótkie zejście w magazyn domowy nie jest tragedią, bo magazyn i tak się doładuje, gdy słońce wyjdzie zza chmury. Rzadsze szarpanie ładowarką jest tego warte.
Efekt zmierzony na symulacji, w tym na prawdziwych nadwyżkach z logów:
| Scenariusz | Przed | Po |
|---|---|---|
| Realne logi (3 minuty) | 5 zmian | 1 |
| Pochmurne 30 minut | 52 zmiany | 1 |
| Stabilne słońce 15 minut | 29 zmian | 0 |
Koszt wolniejszego podbijania mocy to 0,04-0,09 kWh, czyli kilka groszy. Weryfikacja na produkcji potwierdziła rzecz jeszcze ładniejszą: w oknie dwóch i pół minuty produkcja PV spadła chwilowo z 6,8 kW do 1,6 kW, bo przeszła chmura, a prąd nie zmienił się ani razu. Dołek nie utrzymał się przez wymagane dwie iteracje, więc sterownik go zignorował i słońce wróciło.
Wniosek ogólny: przy sterowaniu ze sprzężeniem zwrotnym nie wystarczy poprawnie policzyć wartość zadaną. Trzeba jeszcze zapytać, jak szybko obiekt na nią odpowiada, i nie wysyłać komend częściej. Inaczej regulator ściga własny ogon.
Problem 24: Ładowarka zawieszona przez 36 godzin, a system tego nie zauważył
Ten problem zgłosiłem ja, patrząc na wykres przepływów energii. Cztery kilowaty nadwyżki szły do sieci, auto stało podłączone kablem, a na jego desce rozdzielczej świecił się komunikat „ładowanie zakończone”. Skrypt w tym samym czasie pracował z pozoru wzorowo: liczył nadwyżkę, trzymał tryb solarny i regulował prąd. Najpierw do 9 amperów, potem do 7, potem do 8.
Diagnoza zajęła kilkanaście minut i sprowadziła się do jednego pytania: czy dane, które czytamy, w ogóle są świeże?
Watchdog zgłosił podejrzenie zamrożonych pomiarów cztery razy w ciągu półtorej doby. Za każdym razem dołączał do logu surowy odczyt z ładowarki. Wszystkie cztery były identyczne, co do znaku:
{"L1":[2430,0,0],"L2":[2430,0,0],"L3":[2430,0,0],"t":330,"p":0,"d":0,"e":0}
Napięcie 243,0 V dokładnie takie samo na wszystkich trzech fazach i temperatura obudowy niezmieniona od poprzedniego dnia. Realny pomiar tak nie wygląda. Napięcie w sieci drga o kilka woltów w każdej minucie, trzy fazy praktycznie nigdy nie mają identycznej wartości, a obudowa nagrzewa się i stygnie razem z pogodą. To nie było „auto nie chce się ładować”. To był martwy blok danych.
Reszta obrazu układała się w to samo:
- ładowarka ignorowała polecenia w obie strony: cztery komendy startu poprzedniego dnia i cztery komendy stopu następnego, wszystkie bez najmniejszej reakcji;
- ten sam status przez 641 kolejnych odczytów, bez jednej zmiany mocy;
- ani jednej sekundy z mocą powyżej zera przez 36 godzin, licznik sesji na zerze;
- przy tym zero błędów łączności, a ping do urządzenia wracał w 3 milisekundy.
Ładowarka odpowiadała w sieci, tylko jej oprogramowanie stało. Auto pokazywało „ładowanie zakończone”, bo urządzenie przestało podawać sygnał sterujący i sesja została zamknięta.
Lekarstwo okazało się takie samo jak przy Problemie 14: Reboot z aplikacji Smart Life (Settings, absolutnie nie Reset to Factory). Moc skoczyła z zera na 3700 W w niecałą minutę, przy zupełnie nietkniętym kodzie. To zresztą najczystszy dowód, jaki mogłem dostać: skoro niczego nie zmieniłem w skrypcie, a wszystko ruszyło, to skrypt nie był winowajcą.
Najciekawsze jest jednak to, co ta awaria powiedziała o moim własnym kodzie. Ładowarka zawiesiła się z powodu swojego oprogramowania i na to nie mam wpływu. Ale to, że nikt się o tym nie dowiedział przez półtorej doby, przy dwóch słonecznych dniach z rzędu, było już winą skryptu. Znalazłem siedem osobnych usterek i wszystkie należą do jednej rodziny: system nie odróżniał „urządzenie milczy” od „urządzenie mówi, że wszystko gra”.
- Watchdog rozpoznawał awarię po najsłabszym możliwym sygnale. Patrzył wyłącznie na to, czy moc wynosi zero, co wygląda dokładnie tak samo przy awarii i przy aucie, które jest po prostu naładowane do pełna. Mocniejszy trop leżał w danych przez cały czas: niezmienny surowy odczyt. Teraz porównuję właśnie jego, a najczystszym wskaźnikiem okazała się temperatura, bo ona drga zawsze. Czas wykrycia spadł z „nigdy” do pięciu minut.
- Licznik gubił się przy migotaniu trybu. Liczył się tylko wtedy, gdy skrypt chciał ładować, więc każde chwilowe zejście do bezczynności kasowało go do zera. A przy nadwyżce stojącej na granicy progu tryb przeskakuje tam i z powrotem co kilka minut. W czasie awarii licznik wyzerował się w połowie, przy stanie 40. Ten sam błąd podcinał kompensację poboru, przez co skrypt potrafił zatrzymać realnie trwającą sesję. Jedna poprawka naprawiła oba.
- Jedyną reakcją na awarię było ostrzeżenie w logu. Czyli w miejscu, do którego nikt normalnie nie zagląda. Teraz powstaje powiadomienie w Home Assistant, jedno na zdarzenie, kasowane samo po powrocie ładowarki do pracy.
- Komendy startu i stopu miały osobne liczniki i żaden nie łączył kropek. Każdy z osobna mieścił się w swoim limicie i milkł, choć razem opowiadały jedną historię: to urządzenie nie wykonuje niczego, o co je prosimy.
- „Odpuszczam do zmiany warunków” znaczyło w praktyce „do jutra”. Po trzech nieudanych próbach startu skrypt milkł, a licznik prób zerował się wyłącznie w sytuacji, w której przy trwającej nadwyżce nigdy się nie znajdziemy. Efekt: cisza od wpół do jedenastej do końca dnia, przy nadwyżce dochodzącej do 8,6 kW. Teraz po pół godziny wraca kolejna seria prób.
- Przy aktywnym statusie skrypt nie miał żadnej drogi wznowienia sesji. Warunek, który miał wysłać komendę startu, brzmiał „jeśli ładowarka nie pracuje”, a ona twierdziła, że pracuje. Komenda nie mogła pójść z definicji. To boli podwójnie, bo auta Stellantisa po zatrzymaniu ładowania zamykają sesję i same jej nie wznawiają. Doszedł cykl stop i start po piętnastu minutach bez poboru, z twardym limitem dwóch prób, żeby nie wrócić do klikania stycznikiem z Problemu 13.
- Zadany prąd szedł na ślepo. Ładowarka udostępnia pole z informacją, jaki prąd faktycznie ma ustawiony. Czytałem je, ale nigdy nie porównywałem z tym, co wysłałem. Skrypt posłał 6, 9, 7 i 8 amperów do martwego urządzenia i każdą komendę uznał za wykonaną, bo funkcja wysyłająca nie zgłosiła błędu.
Jest jeszcze ósma poprawka, której nie wymyśliłem przy biurku. Wdrożyłem zmiany, po czym nadeszła chmura i ładowarka przeszła w stan wstrzymania. Zobaczyłem wtedy coś, co powinno być oczywiste: przy zatrzymanym ładowaniu prąd nie płynie, więc pomiary mogą stać w miejscu zupełnie legalnie. Mój świeży licznik zamrożenia rósł jednak niezależnie od tego, czy w ogóle chcemy ładować. Pierwsza minuta po powrocie słońca spełniałaby oba warunki naraz i wyrzucała fałszywy alarm. Codziennie rano. A powiadomienie, które myli się regularnie, przestaje cokolwiek znaczyć, czyli traci się dokładnie to, co się właśnie zbudowało. Poprawka poszła tego samego popołudnia.
Wniosek ogólny, chyba najważniejszy z całego projektu: status z urządzenia to deklaracja, nie fakt. „Pracuję” znaczy tylko tyle, że urządzenie tak twierdzi. Dopóki nie skonfrontuje się tej deklaracji z czymś niezależnym, czyli ze świeżością danych, z potwierdzeniem wykonania komendy i z realnym przepływem mocy, sterownik potrafi godzinami wykonywać bardzo precyzyjne obliczenia na martwym obiekcie i nie mieć o tym najmniejszego pojęcia. Warto pytać nie tylko „co urządzenie mówi”, ale też „kiedy ostatnio powiedziało coś nowego”.
Problem 25: Alarm, którego nikt nie ogląda, nie jest alarmem
Tydzień po naprawieniu poprzedniego problemu ta sama awaria wróciła. Tym razem wykrywanie zadziałało wzorowo: system rozpoznał zamrożone pomiary po pięciu minutach i zaalarmował sześć razy w ciągu dwóch dni. Każdy alarm z surowym odczytem, każdy z gotową instrukcją, co kliknąć.
Awaria trwała dwadzieścia dwie i pół godziny. Do sieci uciekło minimum 12,2 kWh nadwyżki, a drugiego dnia rano ładowarka stała, podczas gdy z dachu szło 7-9 kW.
Dlaczego? Bo powiadomienie trafiało do panelu Home Assistanta, a do panelu nikt nie zagląda w środku dnia roboczego. Diagnostyka była bez zarzutu. Adresat nie istniał.
Pierwszy wniosek jest banalny i właśnie dlatego łatwo go przegapić: wykrycie awarii ma wartość dopiero wtedy, gdy dociera tam, gdzie człowiek naprawdę patrzy. Alarm idzie więc teraz dodatkowo powiadomieniem na telefon. Panel został jako drugi kanał, nie jedyny.
Drugi wniosek jest ciekawszy. Skoro lekarstwo jest znane od maja, zawsze to samo i całkowicie mechaniczne, czyli restart z aplikacji, to dlaczego w ogóle czeka na człowieka? Skrypt wie o awarii pięć minut po jej wystąpieniu, a jedyne, co potrafi, to poprosić o kliknięcie. Zbudowałem więc mechanizm, który restartuje ładowarkę sam: reaktywnie, gdy rozpozna zawieszenie, z limitem trzech prób i dziesięciominutowym odstępem, plus profilaktycznie raz na dobę w nocy, wstrzymany, gdy akurat płynie prąd.
Warto zauważyć, że mój pierwszy pomysł brzmiał „restartujmy co rano” i wcale by nie wystarczył. Tamta awaria zaczęła się w środku dnia, więc poranny restart uratowałby dokładnie nic z popołudnia. Profilaktyka jest higieną, nie mechanizmem ratunkowym.
I tu zaczyna się część, która się nie udała. Okazało się, że wysłanie komendy restartu wcale nie jest trywialne.
Podłączyłem się do ładowarki podsłuchem odpytującym ją trzy razy na sekundę i poprosiłem domownika o kliknięcie restartu w aplikacji. Wynik zaskoczył mnie na trzy sposoby. Po pierwsze, żadne z widocznych pól nie drgnęło. Po drugie, urządzenie ani na moment nie zniknęło z sieci, co znaczy, że „restart” wcale nie restartuje całego sprzętu, a jedynie moduł wykonawczy. Po trzecie, widać było za to jego skutek: napięcie sygnału sterującego, którym ładowarka rozmawia z autem, spadło na trzy sekundy do zera.
Przy okazji wyszło, że pole, które przez trzy miesiące uważałem za numer wersji, jest w rzeczywistości napięciem tego właśnie sygnału. Około 12 V znaczy „auto odpięte”, 9 V „podłączone”, 6 V „ładuje”. Ładowarka przez cały czas mówiła mi, czy kabel siedzi w gnieździe, a ja tego nie czytałem.
Skoro komendy nie widać w odczycie, musi być tylko do zapisu. I to nie jest teoria: pole, którym od miesięcy włączam i wyłączam ładowanie, też nie pojawia się w żadnym odczycie ani skanie, a działa bez zarzutu. Takich komend po prostu nie da się podsłuchać.
Zostało zapytać producenta. Chmura zna pełny model urządzenia, więc założyłem konto deweloperskie i wyciągnąłem listę wszystkich pól z ich oryginalnymi nazwami. Znalazłem tam dokładnie to, czego szukałem: pole nazwane wprost „wykonaj restart”. Wysłałem. Nic. Wysłałem przez chmurę. Chmura potwierdziła przyjęcie, urządzenie nie zareagowało.
Po kilkunastu podejściach trafiłem wreszcie na kombinację, która zadziałała: nie samo ustawienie wartości, lecz pełna zmiana stanu, najpierw wyłącz, potem włącz. Napięcie sygnału spadło do zera dokładnie sekundę po komendzie. Sukces. Tyle że jedyny. Pięć kolejnych prób, w tym po dłuższych przerwach, nie dało nic.
Jeden sukces na sześć prób to nie jest mechanizm, tylko anegdota. Automat ma mnie ratować przed dobową awarią, więc nie może opierać się na komendzie działającej raz na kilka razy, w okolicznościach, których nie umiem odtworzyć. Mechanizm jest więc gotowy, przetestowany i uśpiony: czeka na jedną brakującą stałą.
I wtedy przyszła odpowiedź, dlaczego nic z tego nie działało. Okazało się, że mam w Home Assistancie integrację, która potrafi odpytać chmurę Tuya kanałem aplikacyjnym, czyli tym samym, którego używa Smart Life. To ważne rozróżnienie: przez cały dzień wysyłałem nazwę komendy wziętą z portalu deweloperskiego, ani razu nie sprawdzając, czy kanał aplikacji nazywa ją tak samo.
Zapytałem więc wprost o specyfikację urządzenia. Odpowiedź była krótka: pusta lista funkcji, pusta lista punktów danych, a kategoria urządzenia ustawiona na „lampa”.
Chmura po prostu nie ma modelu tej ładowarki. To jedno odkrycie tłumaczy wszystkie niepowodzenia naraz. Integracja nie stworzyła żadnego przycisku, bo nie miała z czego. Komendy wysyłane po nazwie nie przechodziły, bo żaden kanał nie kojarzy ich z tym egzemplarzem. A chmura odpowiadała „wykonano” i nie robiła nic, bo przyjmowała polecenie, którego nie miała komu przekazać. Sprawdziłem jeszcze wariant z numerem punktu zamiast nazwy - również cisza.
Wniosek: aplikacja producenta steruje tym urządzeniem własnym panelem, który omija publiczne API. Dostaje interfejs szyty na miarę, a świat zewnętrzny widzi lampę bez funkcji. Dostępnymi narzędziami tego nie odtworzę i na tym kończę poszukiwania.
Drobiazg wart zapamiętania przy podobnych polowaniach: akcja, której użyłem, nazywa się „wywołaj API i zwróć wynik”, ale wyniku nie zwraca. Trzeba podnieść poziom logowania integracji i szukać odpowiedzi w logu systemu. Dobrą godzinę patrzyłem na zielony znaczek bez treści, zanim to sprawdziłem - a odpowiedź, której szukałem cały dzień, leżała jedno ustawienie dalej.
Jedna rzecz z tego etapu jest warta zapamiętania jako metoda. Pierwszą próbę uznałem za obiecującą, bo po komendzie ładowarka kilka razy przestała odpowiadać, a chmura wepchnęła swoje ustawienia, czyli dokładnie to, co widuję po prawdziwym restarcie. Dopiero próba kontrolna, czyli identyczny pomiar bez wysyłania czegokolwiek, pokazała, że to był zwykły szum. Bez niej wpisałbym do kodu komendę, która nic nie robi, i dowiedziałbym się o tym przy następnej awarii, czyli w najgorszym możliwym momencie. Przy pomiarach na urządzeniu, które ktoś inny również odpytuje, wynik bez grupy kontrolnej jest wart tyle co nic.
Z tego samego podsłuchu wypadła jeszcze jedna poprawka, tym razem zupełnie przypadkiem. Ładowarka przy starcie sesji przechodzi przez krótki stan pośredni, trwający kilkanaście sekund, który znaczy „kabel włożony”. Nie miałem go na żadnej liście, więc skrypt widział wtedy status, którego nie rozpoznawał, i uznawał, że auto jest odpięte. Przy pętli co pół minuty to od czasu do czasu jedna iteracja bez sterowania, czyli drobiazg. Gorszy jest wariant, w którym ładowarka zawiesiłaby się właśnie w tym stanie: watchdog by tego nie zobaczył, bo zamrożenie liczy się wyłącznie wtedy, gdy skrypt w ogóle chce ładować. Cicha dziura dokładnie tej samej rodziny co cały poprzedni problem.
Wniosek na koniec: dwie z trzech rzeczy, które wyniosłem z tej sesji, nie wzięły się z naprawiania tego, co zepsute, tylko z przyglądania się urządzeniu przy normalnej pracy. Napięcie sygnału sterującego i ten stan pośredni leżały w danych od miesięcy. Wystarczyło raz spojrzeć z rozdzielczością większą niż co trzydzieści sekund.
Problem 26: Alarm o awarii, której nie było
Pięć dni po poprzedniej naprawie zgłosiłem podejrzenie, że coś jest nie tak z uruchamianiem ładowania. Pamiętałem mało słońca, słabą produkcję, coś, co ledwo ruszyło, i mocno rozładowany magazyn domowy.
Logi pokazały co innego i to była pierwsza lekcja: pamięć nie jest dobrym źródłem danych diagnostycznych. Tamtego dnia przez pięć godzin nadwyżka sięgała 5-8 kW, produkcja dochodziła do 8,4 kW, a magazyn stał na 100%. Warunki idealne. Ładowanie nie ruszyło ani razu.
Objawy, które zapamiętałem, były prawdziwe, tylko dotyczyły dwóch minut, a nie całego popołudnia. Sesja w końcu ruszyła, ale słońce zdążyło schować się za chmury: produkcja spadła do 700 W, a ładowarka pobierała 3,8 kW. Różnica szła z magazynu. Skrypt policzył to poprawnie i w ciągu trzydziestu sekund wysłał komendę zatrzymania, a ładowarka wykonała ją dopiero po dwóch minutach. Stąd rozładowany magazyn: około 0,13 kWh.
Prawdziwe znalezisko było gdzie indziej. Samochód przez te pięć godzin w ogóle nie był podpięty. A skrypt w tym czasie wysłał 72 komendy startu do pustego gniazda, wygenerował siedemdziesiąt ostrzeżeń w logu i rzucił dwa fałszywe alarmy o awarii, z których pierwszy wisiał pięć godzin.
Przyczyna okazała się wstydliwie prosta. Ładowarka ma dwa stany, które znaczą „nie widzę żadnego auta". W moim kodzie oba figurowały na liście stanów gotowości do ładowania. Skrypt wchodził więc w tryb solarny, wysyłał komendę startu, a mechanizm nadzoru widział „chcemy ładować, a pomiar stoi w miejscu" i uznawał to za zawieszenie. Pomiar stał, bo prąd nie płynął. Nie było czym.
I to jest gorsze niż zmarnowana energia. Cała wartość powiadomienia opiera się na tym, że kiedy przychodzi, to coś znaczy. Alarm, który myli się przy każdym odpiętym aucie w słoneczny dzień, po tygodniu przestaje się czytać - a wtedy przepada ten jeden prawdziwy, dla którego cały mechanizm powstał. Zbudowałem sobie pasterza, który woła „wilk".
Naprawa wzięła się z rzeczy, którą tydzień wcześniej zapisałem jako ciekawostkę bez zastosowania: z napięcia sygnału sterującego, którym ładowarka rozmawia z autem. Ona przez cały czas mówi, czy kabel siedzi w gnieździe. Około 12 V znaczy „pusto", 9 V „podłączone", 6 V „ładuję". Wystarczyło zacząć to czytać i pytać urządzenie o fakt, zamiast wnioskować ze statusu, który znaczył co innego, niż zakładałem.
Od tej pory bez auta skrypt nie wysyła ani jednej komendy i nie alarmuje o niczym. Gdy napięcia nie da się odczytać, zachowuje się jak dawniej - brak danych nie może unieruchomić sterowania, bo lepiej wysłać zbędną komendę niż przegapić realną nadwyżkę.
Jedna rzecz świadomie została niedokończona. Chciałem dorzucić zabezpieczenie na wypadek, gdyby awaria zamroziła samo to napięcie: ładowarka twierdziłaby wtedy w nieskończoność, że auta nie ma, a skrypt by jej wierzył. Zaplanowałem alarm po godzinie takiego stanu i odrzuciłem ten pomysł po sprawdzeniu danych. Przy odpiętym aucie pomiar realnie stoi godzinami - tamtego dnia ten sam odczyt trwał od wpół do jedenastej do wpół do czwartej. Takie zabezpieczenie produkowałoby dokładnie te fałszywe alarmy, które właśnie usuwałem. Lepszy jawnie opisany brak pokrycia niż mechanizm, który udaje ochronę, a w praktyce hałasuje.
Wniosek: warto oddzielać „urządzenie nie robi tego, co chcę" od „urządzenie nie ma z kim pracować". Brzmi banalnie, dopóki nie okaże się, że przez trzy miesiące traktowało się jedno jak drugie - i że nadzór, który miał pilnować systemu, sam stał się źródłem fałszywych sygnałów.
Helpery w Home Assistant
Wymagane helpery — tworzone przez UI (Settings → Helpers):
| Typ | Entity ID | Opis |
|---|---|---|
| Text | input_text.ev_charger_status | Status ładowarki (WORKING/SLEEP/PAUSE…) |
| Text | input_text.ev_charger_mode | Aktywny tryb (SOLAR/EMERGENCY…) |
| Text | input_text.ev_data | JSON z pełnymi danymi sesji |
| Toggle | input_boolean.ev_tryb_zimowy | Tryb zimowy — nocne ładowanie 22–6 |
| Toggle | input_boolean.ev_tryb_awaryjny | Tryb awaryjny — ładuj na maksa teraz |
| Number | input_number.ev_awaryjny_godziny | Czas trybu awaryjnego (0,5–8h) |
| Button | input_button.ev_archiwizuj_teraz | Ręczna archiwizacja bieżącego miesiąca (opcjonalny) |
Efekty i wnioski
Strategia sezonowa — lato i zima
System jest zaprojektowany na cały rok z jednym przełącznikiem sezonowym.
Lato (kwiecień–wrzesień):
Polska ma dobre nasłonecznienie — 9 kWp produkuje regularnie nadwyżki powyżej 1,6 kW. Auto ładuje się za darmo z nadwyżek PV. Przy ujemnych cenach Pstryk (które latem zdarzają się regularnie w południe) system ładuje na 13A, czyli około 9 kW, a operator energii dopłaca za pobieranie prądu.
Zima (październik–marzec):
Krótkie dni, niskie słońce — nadwyżki PV są rzadkie i małe. Jednocześnie od października planowana jest taryfa G12W z tanią energią nocną (~0,70 zł/kWh vs ~0,85 zł/kWh w dzień). Włączam jeden przełącznik w HA — ❄️ Tryb zimowy — i skrypt automatycznie ładuje auto w nocy między 22:00 a 6:00 na 10A (~6,9 kW).
Dlaczego 10A a nie 16A? Zimą działają pompy ciepła powietrze-powietrze które mogą pobierać łącznie 3–4 kW. Przy przyłączu 11 kW zostaje bezpiecznie ~7 kW na auto, ale przyjąłem 10A (6,9 kW) jako bezpieczny bufor na szczyty poboru (gotowanie, bojler, klimatyzatory).
Słoneczne dni zimą? Skrypt nadal wykrywa nadwyżki PV i uruchamia tryb SOLAR automatycznie — tryb zimowy dodaje tylko nocne okno ładowania, nie wyłącza logiki solarnej.
Przy ujemnych cenach Pstryk (które latem zdarzają się regularnie w godzinach 10:00–16:00) system automatycznie ładuje auto na maksimum. W majowy dzień cena spadła do -0,60 zł/kWh — za każdą godzinę ładowania (9,8 kWh) operator energii płacił mi 5,88 zł zamiast żebym ja płacił.
Ładowanie z nadwyżek działa dokładnie tak jak planowałem — gdy bateria jest pełna i słońce produkuje więcej niż potrzeba, auto dostaje resztę. Prąd reguluje się co 30 sekund, typowo oscyluje w zakresie 8–12A przy produkcji PV 8 kW.
Historia miesięczna — przeglądanie miesiąc do miesiąca
Przez pierwszych kilka tygodni dashboard pokazywał statystyki tylko z bieżącego miesiąca. Mankament: zarówno wewnętrzny licznik energii naładowanej do auta, jak i miesięczne liczniki utility_meter w Home Assistant zerują się 1. dnia każdego miesiąca — a stara wartość trafiała wyłącznie do logu i przepadała. Nie dało się cofnąć w czasie i porównać: ile auto wzięło z PV w maju, a ile w czerwcu.
Dorzuciłem więc trwałe archiwum miesięczne z retencją 10 lat. Tuż przed wyzerowaniem licznika skrypt zapisuje zamknięty miesiąc jako jeden rekord:
- energia naładowana do auta [kWh],
- produkcja PV, zużycie domu, import i eksport z sieci [kWh],
- samowystarczalność energetyczna domu [%].
Przykładowy wpis za czerwiec 2026: 145,86 kWh wpompowane w auto przy 62,3% samowystarczalności. Na dashboardzie wyświetlam to jako wykres słupkowy (miesiąc do miesiąca: auto vs produkcja PV vs zużycie domu) oraz tabelę porównawczą.
Diabeł tkwił w dwóch szczegółach, które warto znać:
Wyścig z resetem. Gdyby przy przełomie miesiąca odczytać liczniki „na bieżąco”, można trafić już po ich wyzerowaniu i zapisać zera. Dlatego skrypt w każdej iteracji (co 30 s) zapamiętuje snapshot liczników, a przy przełomie archiwizuje snapshot z poprzedniej iteracji — czyli stan na koniec starego miesiąca. Niezależnie od tego, w jakiej kolejności HA zresetuje utility_meter.
set_state() kontra HA 2026. Pierwsza wersja publikowała sensor archiwum przez AppDaemonowe set_state() — i dostawała 400 Bad Request. Diagnostyka (strzał wprost do REST API rdzenia przez proxy supervisora) pokazała, że samo API przyjmuje identyczny payload bez zająknięcia — wina leżała po stronie ścieżki set_state. Ostatecznie publikuję sensor bezpośrednim POST-em do REST API, z całym archiwum w atrybucie months (atrybuty nie mają limitu 255 znaków, w przeciwieństwie do input_text i stanu encji). Szczegóły w Problemach 17–18 powyżej.
Źródłem prawdy jest plik JSON, który przeżywa restarty — sam sensor to tylko warstwa prezentacji, odtwarzana przy każdym starcie skryptu. Dodałem też opcjonalny przycisk „Zarchiwizuj bieżący miesiąc”, który robi snapshot niezamkniętego miesiąca od ręki (bez resetu liczników) — przydatny, gdy nie chce się czekać do 1. dnia, żeby zobaczyć dane.
Audyt kodu, czyli co siedziało w skrypcie przez trzy miesiące
Skrypt działał od maja i robił swoje, więc przez długi czas nie było powodu do niego zaglądać. W lipcu usiadłem do porządnego przeglądu całości: linijka po linijce, z pytaniem „czy to na pewno robi to, co myślę”. Wyszły cztery błędy, z czego dwa realnie kosztowały mnie energię z magazynu domowego. Żaden nie rzucał się w oczy w logach, bo żaden nie powodował awarii. Po prostu system zachowywał się odrobinę inaczej, niż sądziłem.
Najciekawszy okazał się błąd w samej regulacji. Gdy nadeszła chmura i zaczynałem pobierać prąd z sieci, skrypt zamiast zejść z mocy ładowania, podkręcał ją. Krok po kroku: 6A, 7A, 8A, aż do maksimum. Powód jest podręcznikowy i dlatego wart opisania: przy imporcie kod gubił informację o tym, jak duży jest deficyt, i podstawiał w to miejsce stałą wartość. A ponieważ do nadwyżki dolicza się moc ładowarki, każda kolejna iteracja widziała „więcej dostępnej mocy” niż poprzednia. Klasyczne dodatnie sprzężenie zwrotne, w pętli, którą sam napisałem i której przez kwartał nie zauważyłem. Ładowanie kończyło się dopiero wtedy, gdy magazyn domowy spadł poniżej 95% i wchodził tryb priorytetu baterii, czyli już po niepotrzebnym cyklu rozładowania.
Przy okazji wyszła rzecz, która zmieniła moje rozumienie własnej instalacji. Licznik na złączu z siecią nie mówi prawdy o nadwyżce, jeśli ma się falownik hybrydowy. Sofar w trybie autokonsumpcji aktywnie dopełnia deficyt z magazynu, żeby utrzymać zerowy bilans z siecią. Efekt jest taki, że przy PV 1 kW, domu 5 kW i aucie ciągnącym 4 kW licznik pokazuje spokojne zero, a bateria w garażu po cichu się opróżnia. Teraz nadwyżkę liczę jako minimum z dwóch rzeczy: tego, co faktycznie wypycham do sieci, i tego, co zostaje z produkcji po odjęciu zużycia domu. Pierwsze pilnuje, żeby nie podbierać mocy ładującej się baterii, drugie widzi deficyt, który bateria maskuje.
Drugi poważny błąd był bardziej perfidny, bo powstał przy naprawianiu innego błędu. W maju walczyłem z tym, że skrypt co 30 sekund wysyłał do wallboxa komendę STOP i słychać było klikanie stycznika. Naprawa była prosta: zapamiętuj, co ostatnio wysłałeś, i nie powtarzaj. Tyle że zapamiętywanie działo się także wtedy, gdy wysyłka się nie udała. Wystarczył jeden zgubiony pakiet Wi-Fi (a wallbox stoi w garażu, zasięg bywa marny), żeby skrypt do końca życia procesu był przekonany, że komendę wysłał. W praktyce: ładowanie nie ruszało, dopóki czegoś nie zrestartowałem, albo, w drugą stronę, auto ładowało się mimo trybu priorytetu baterii.
Ten drugi wariant potwierdził się w najlepszy możliwy sposób: dokładnie w chwili wgrywania poprawki. Auto ciągnęło wtedy 5,2 kW przy magazynie naładowanym w 46%, czyli w sytuacji, w której skrypt od dawna powinien był je zatrzymać. Nie zatrzymywał, bo miał zapisane, że już to zrobił. Nowa wersja ucięła sesję w pierwszej iteracji po restarcie. Trudno o lepszy dowód, że błąd nie był teoretyczny.
Do tego doszły dwie rzeczy z gatunku „cicha awaria”. Biblioteka TinyTuya przy problemach z siecią nie zgłasza wyjątku, tylko zwraca słownik z kluczem Error, a stary kod interpretował to jako „ładowarka gotowa do pracy”. I plik z licznikami energii oraz dziesięcioletnim archiwum zapisywał się nieatomowo, więc jedno przerwanie w złym momencie mogło go uszkodzić tak, że od tej pory każdy kolejny zapis cicho padał, a liczniki wracały do zera.
Czego się nauczyłem o testowaniu takich systemów
Napisałem do skryptu zestaw prostych testów, bez żadnego frameworka, podmieniając AppDaemon i TinyTuya atrapami. Ale najwięcej dała nie tabelka testów, tylko symulacja całego dnia: słońce, chmura, powrót słońca, wieczór, z wallboxem reagującym na komendy jak prawdziwy. Dopiero ona pokazała błąd, którego testy jednostkowe nie widziały, bo dotyczył kolejności operacji. Uśrednianie odczytów robiłem przed doliczeniem poboru ładowarki, przez co średnia mieszała próbki mierzone przy różnej mocy ładowania i tuż po starcie sesji prąd skakał na maksimum.
Wniosek na przyszłość jest chyba taki: przy sterowaniu ze sprzężeniem zwrotnym sprawdzanie pojedynczych funkcji to za mało. Trzeba puścić pętlę w czasie i zobaczyć, dokąd zbiega. Po poprawkach symulacja wygląda tak, jak powinna: 11A stabilnie w pełnym słońcu, przy chmurze redukcja 10, 8, 7, 6 amperów, stop, a potem płynny powrót w górę.
Dopisek z następnego dnia: ta sama historia dostała ciąg dalszy, którego nie wychwyciły ani testy, ani symulacja. Zgłosiło go ucho - usłyszałem przez okno, że ładowarka pika co pół minuty. Regulacja liczyła poprawnie, tylko wysyłała komendy znacznie częściej, niż auto było w stanie za nimi nadążyć. Opisałem to jako Problem 23 wyżej. Morał: nawet dobra symulacja nie zastąpi tego, że system stoi w garażu i wydaje dźwięki.
Koszt całego rozwiązania
| Element | Koszt |
|---|---|
| Ładowarka dé EV 11kW Wi-Fi | ~1150 zł |
| Home Assistant | 0 zł (open source) |
| AppDaemon | 0 zł (open source) |
| TinyTuya | 0 zł (open source) |
| Tuya IoT Platform (jednorazowe pobranie klucza) | 0 zł |
Łącznie: 1150 zł za inteligentną ładowarkę zintegrowaną z PV.
Dla porównania — dedykowane ładowarki z zarządzaniem mocą i integracją z PV kosztują 3000–8000 zł.
Dla technicznych: kluczowe fragmenty kodu
Pełny skrypt AppDaemon dostępny na moim GitHubie: github.com/tomasz-kwietniewski/ha-ev-charger. Dane urządzenia (Device ID, Local Key, IP) trzymam w osobnym pliku ev_charger_secrets.json który nie trafia do repozytorium — szablon znajdziesz w repo jako ev_charger_secrets.json.example. Poniżej kluczowe fragmenty kodu:
Odczyt danych z ładowarki z obsługą PAUSE:
CHARGER_READY_STATES = {"PAUSE", "SLEEP", "IDLE", "UNKNOWN"}
CHARGER_WORKING_STATES = {"WORKING"}
def _get_charger_data(self):
raw = self._device.status()
# tinytuya zwraca błąd jako słownik, nie wyjątek (Problem 21)
if not isinstance(raw, dict) or "Error" in raw:
raise RuntimeError(f"tinytuya zwrocil blad: {raw!r}")
dps = raw.get("dps", {})
status = str(dps.get("109", "unknown")).upper()
current = int(dps.get("150", 0))
metrics = json.loads(dps.get("102", "{}"))
l1 = metrics.get("L1", [0, 0, 0])
l2 = metrics.get("L2", [0, 0, 0])
l3 = metrics.get("L3", [0, 0, 0])
power_w = (l1[2] + l2[2] + l3[2]) * 100 # skala x100!
return {"status": status, "current_a": current, "power_w": power_w}
Obliczanie nadwyżki z uśrednianiem:
# Sofar: dodatni PCC = eksport (nadwyżka), ujemny = import.
# min() bierze wariant konserwatywny: PV minus dom widzi deficyt
# maskowany przez magazyn, PCC pilnuje mocy idącej do baterii.
surplus_without_ev_kw = min(grid_power, pv_power - load_power)
# Pobór auta siedzi już w load_power, doliczamy go z powrotem PRZED
# uśrednianiem, żeby każda próbka w historii znaczyła to samo.
available_kw = surplus_without_ev_kw + charger_power_kw
# Uśredniamy ostatnie 3 odczyty (90s) żeby wyeliminować migotanie
self._surplus_history.append(available_kw)
if len(self._surplus_history) > PCC_HISTORY_SIZE:
self._surplus_history.pop(0)
avg_available_kw = sum(self._surplus_history) / len(self._surplus_history)
# Bias +1000W, agresywniejsze wykorzystanie nadwyżek.
# BEZ podłogi: przy imporcie wychodzi ujemne, więc regulacja redukuje prąd.
surplus_w = avg_available_kw * 1000 + SURPLUS_BIAS_W
Logika decyzyjna z sześcioma trybami:
def _decide(self, ha_data, charger_data):
# 1. EMERGENCY — najwyższy priorytet
if self._is_emergency_active():
if soc < SOC_EMERGENCY_MIN:
return ("BATTERY_PRIORITY", 0)
return ("EMERGENCY", EMERGENCY_CURRENT_A) # 13A
# 2. Ujemna cena energii
if price < 0:
return ("NEGATIVE_PRICE", NEGATIVE_PRICE_CURRENT_A) # 13A
# 3. Tryb zimowy — nocne ładowanie
if winter_mode and in_night_window:
return ("WINTER_NIGHT", WINTER_MAX_CURRENT) # 10A
# 4. Ochrona baterii
if soc < SOC_THRESHOLD: # 95%
return ("BATTERY_PRIORITY", 0)
# 5. Tryb solarny
if available_surplus >= START_SURPLUS_W: # 1600W
current = max(6, min(16, int(available_surplus / (3 * 230))))
return ("SOLAR", current)
return ("IDLE", 0)
Tryb EMERGENCY z automatycznym timerem:
def _on_emergency_toggle(self, entity, attribute, old, new, kwargs):
if new == "on":
hours = self._get_emergency_hours() # z input_number
self._emergency_end_time = datetime.datetime.now() + datetime.timedelta(hours=hours)
self._clear_schedule() # wyczyść harmonogram przed startem
else:
self._emergency_end_time = None
def _is_emergency_active(self):
if self.get_state(EMERGENCY_MODE_ENTITY) != "on":
return False
if datetime.datetime.now() > self._emergency_end_time:
# Czas minął — wyłącz automatycznie
self.call_service("input_boolean/turn_off", entity_id=EMERGENCY_MODE_ENTITY)
return False
return True
Podsumowanie
Inteligentne ładowanie auta elektrycznego z nadwyżek PV nie wymaga drogiego sprzętu. Wystarczy:
- Tania ładowarka z Wi-Fi i protokołem Tuya (~1150 zł)
- Home Assistant jako centrum automatyki
- Biblioteka TinyTuya do lokalnej kontroli
- Trochę Pythona w AppDaemon
System obsługuje sześć trybów pracy: solarny (proporcjonalnie do nadwyżek), awaryjny (ładuj teraz na maksa), ujemne ceny (operator płaci), zimowy (nocna taryfa), priorytet baterii i bezczynność. Wszystko sterowane z poziomu dashboardu HA.
Efekt: auto ładuje się za darmo gdy świeci słońce, a przy ujemnych cenach Pstryk — operator energii dopłaca za to, że pobieramy prąd.
Latem planujemy naładować całą baterię 75 kWh praktycznie bez kosztów. Policzymy to jesienią.
Artykuł napisany na podstawie rzeczywistej instalacji. Pierwsza wersja: maj 2026. Aktualizacja: maj 2026 — dodano tryb EMERGENCY, obsługę stanu PAUSE, uśrednianie PCC, obniżenie progu startu do 1600W. Aktualizacja 2: maj 2026 — uśrednianie PCC rozszerzone do 3 próbek (90s), bias wydzielony jako nazwana stała SURPLUS_BIAS_W, poprawka komentarzy znaku PCC. Aktualizacja 3: 12 maja 2026 — dodano Problem 12 (AppDaemon skanuje apps/ rekurencyjnie — duplikaty aplikacji przy backupie wewnątrz folderu). Aktualizacja 4: 8 czerwca 2026 — Problemy 13–16 (STOP-spam w gałęzi IDLE, zamrożony DP 102 w firmware dé EV v2.9.4, chmura Tuya a harmonogram DP 151, ukryte pole e = energia sesji × 0,1 kWh); archiwum historii miesięcznej z retencją 10 lat — wykres i tabela porównawcza na dashboardzie, ręczny przycisk archiwizacji (Problemy 17–18: dane ginące przy resecie miesiąca oraz set_state 400 w HA 2026.x → publikacja przez REST API rdzenia). Aktualizacja 5: 27 lipca 2026 — audyt kodu, Problemy 19-22: regulacja SOLAR „uciekająca” w górę przy zachmurzeniu (nadwyżka liczona teraz jako minimum z eksportu i z produkcji minus zużycie domu, bez podłogi), dedup komend START/STOP bez ponowień, TinyTuya zwracająca błąd jako słownik zamiast wyjątku, nieatomowy zapis pliku z licznikami; tryb ujemnych cen zszedł z 16A na 13A (bufor na dom), doszły testy jednostkowe i symulacja pętli regulacji. Aktualizacja 6: 28 lipca 2026 - Problem 23: regulacja goniąca szum (prąd zmieniany co 30 sekund, sekwencje 10A, 11A, 10A). Histereza plus minus 250 W wokół progu stopnia oraz potwierdzenie zmiany przez dwie iteracje; duży spadek nadal natychmiastowy. Zmierzone: 52 zmiany prądu w pochmurne pół godziny zeszły do jednej. Aktualizacja 7: 11 sierpnia 2026 - Problem 24: ładowarka zawieszona przez 36 godzin (odpowiadała w sieci, ale nie aktualizowała danych i ignorowała wszystkie komendy), a system tego nie zauważył. Rozpoznawanie awarii po niezmiennym surowym odczycie pomiarów zamiast po samym zerze mocy, powiadomienie w Home Assistant zamiast ostrzeżenia w logu, cykl budzenia sesji, gdy ładowarka twierdzi że pracuje, a prąd nie płynie, potwierdzanie zadanego prądu, koniec z trwałym odpuszczaniem prób startu. Testy jednostkowe wzrosły z 24 do 51. Aktualizacja 8: 20 sierpnia 2026 - Problem 25: ta sama awaria wróciła i mimo sześciu poprawnych alarmów trwała 22,5 godziny, bo powiadomienie szło wyłącznie do panelu Home Assistanta. Alarm idzie teraz również na telefon. Doszedł mechanizm automatycznego restartu ładowarki, na razie uśpiony: komenda restartu okazała się poleceniem tylko do zapisu, którego nie da się podsłuchać, a to znalezione w modelu producenta zadziałało raz na sześć prób. Przy okazji: pole, które uważałem za numer wersji, jest napięciem sygnału sterującego między ładowarką a autem, oraz naprawiony stan pośredni przy starcie sesji, przez który skrypt widział podłączone auto jako odpięte. Testy jednostkowe: 51 do 67. Poszukiwania komendy restartu zamknięte: chmura Tuya nie ma modelu tego wallboxa (pusta lista funkcji, kategoria ustawiona na lampę), więc ani kanał deweloperski, ani aplikacyjny nie kojarzy z nim żadnej komendy - aplikacja producenta steruje nim własnym panelem, omijającym publiczne API. Aktualizacja 9: 25 sierpnia 2026 - Problem 26: skrypt wysyłał komendy startu do pustego gniazda i rzucił dwa fałszywe alarmy o awarii, bo dwa stany ładowarki znaczące „nie widzę auta" traktował jako gotowość do ładowania. Naprawione odczytem napięcia sygnału sterującego - tego samego, które tydzień wcześniej zapisałem jako ciekawostkę bez zastosowania. Testy jednostkowe: 67 do 75.