← Zápisky

Odečítání elektroměru ZPA ZE314 s ESPHome

Obsah článku

Už delší dobu jsem chtěl vědět, kolik elektřiny náš dům spotřebuje. Jednak máme elektrický kotel, který ohřívá vodu i podlahové topení, a jednak mě baví mít všechno možné v Home Assistantu a sledovat to v čase. V hlavním rozvaděči je elektroměr ZPA ZE314 od distributora – třífázový a dvoutarifní. Zásuvka poblíž není, takže volba padla na ESP32 na baterii.

Výsledek: ESP32-C3 s optickou hlavou každých 30 minut přečte stav elektroměru, zkontroluje ho a pošle do Home Assistantu. Níž je protokol, data, která elektroměr vrací, chyby, na které jsem narazil, zapojení a firmware.

První krok: Python na notebooku

Nejdřív jsem potřeboval zjistit, jak data z elektroměru vůbec dostat. Odečet tužkou na papír jednou za měsíc (pokud jsem nezapomněl) mě už nebavil. ZE314 má optický port podle IEC 62056-21, na který se magnetem přichytí čtecí hlava – a jednu starou jsem našel v krabici se součástkami.

Připojil jsem ji k notebooku a začal data vyčítat v Pythonu. Vznikl z toho skript meter_read.py a shellový capture.sh, který ho pouští opakovaně a měření ukládá do JSON:

meter_read.py -p /dev/cu.usbserial-XXX                 # úvodní registry, 0,18 s
meter_read.py -p /dev/cu.usbserial-XXX --mode full     # všech 183 registrů, s kontrolou BCC
meter_read.py -p /dev/cu.usbserial-XXX --listen 60     # jen poslouchá, nic neposílá
capture.sh 30 /dev/cu.usbserial-XXX 5 full "poznámky"  # série měření do JSON pro regresní testy

Později přibyl simulátor elektroměru meter_sim.py. Běží na PC, přehrává skutečný záznam a umí záměrně vyrobit chybu: chybějící nebo useknutou identifikaci, useknutý blok, výpadek části bloku, špatný kontrolní součet (BCC), nesedící tarify, klesající nebo skokově rostoucí stav. Skutečný elektroměr na požádání neselže, takže bez simulátoru by se chybové větve firmwaru nikdy nevyzkoušely. Každá z těchto chyb musí skončit tak, že se do Home Assistantu nepošle nic.

Optické rozhraní a IEC 62056-21

Elektroměr komunikuje v režimu C s textovým výpisem, vždy 7 datových bitů, sudá parita a 1 stop bit (7E1). Výměna probíhá takto:

KrokSměrBajtyRychlost
1→ elektroměr/?!\r\n300 Bd
2← elektroměr/ZPA5ZE314.v61_012\r\n300 Bd
3→ elektroměr06 30 35 30 0D 0A (ACK, V=0 Z=5 Y=0)300 Bd
4–počkat ~200 ms, přepnout port–
5← elektroměrSTX <řádky> ! CR LF ETX BCC9600 Bd

Pátý znak identifikace (5) říká, že elektroměr umí 9600 Bd. ACK s Z=5 tuhle rychlost potvrdí a Y=0 vyžádá jen výpis; Y=1 (programovací režim) se nikdy neposílá. Výpis má 4656 znaků: při 9600 Bd trvá 4,85 s, při 300 Bd by trval 155 s.

Elektroměr ZPA ZE314 s optickou hlavou přichycenou na optickém portu

První zádrhel: po přepnutí na 9600 Bd jsem vyprázdnil vstupní buffer a přišel tím o STX, tedy o začátek výpisu. Echo vlastního dotazu je potřeba vyčíst ještě při 300 Bd.

Co elektroměr vrací

Odpověď má 183 registrů: 33 aktuálních hodnot a 150 měsíčních snímků historie. Toto je zkrácený skutečný výpis ze 14. srpna 2026; sériové číslo a adresu zařízení jsem zamaskoval, […] označuje vynechané řádky.

/ZPA5ZE314.v61_012
<STX>
C.1.0(XXXXXXXXXX)
0.0.0(XXXXXXXXXXXXXXXX)
0.3.0(10000*imp/kWh)
0.3.3(00100*imp/kWh)
F.F(000000)
1.8.0(0086889.849*kWh)
1.8.1(0023133.294*kWh)
1.8.2(0063756.555*kWh)
1.8.1-01(0023099.738*kWh)
1.8.1-02(0023053.068*kWh)
[…]
21.8.0(0029055.427*kWh)
41.8.0(0030171.146*kWh)
61.8.0(0027663.275*kWh)
2.8.0(0000001.030*kWh)
[…]
1.6.1(0009.03*kW,2608060930)
1.6.2(0010.53*kW,2608091130)
[…]
C.8.1(02812801)
C.8.2(04986131)
C.8.0(07319723)
C.82.0(00000548)
C.7.1(00000011)
C.7.2(00000007)
C.7.3(00000162)
[…]
0.2.1(ver.01, 160719, 4AE1)
C.2.1(1705271747)
C.2.9(2608141841)
0.9.2(260814)
0.9.1(184330)
C.51.15(0000000000)
[…]
C.6.0(0003242147)
C.6.3(3.65V)
!
<ETX><BCC>

Co jednotlivé řádky (OBIS kódy) znamenají:

OBISHodnota v ukázceVýznam
C.1.0(zamaskováno)sériové číslo elektroměru
F.F000000stav fatální chyby, 0 = v pořádku
1.8.086 889,849 kWhodběr celkem (A+)
1.8.123 133,294 kWhodběr v tarifu T1
1.8.263 756,555 kWhodběr v tarifu T2
21.8.0 / 41.8.0 / 61.8.029 055 / 30 171 / 27 663 kWhodběr po fázích L1 / L2 / L3
2.8.01,030 kWhdodávka do sítě (A−)
1.6.1 / 1.6.29,03 / 10,53 kWmaximální odebíraný výkon T1 / T2 s časem YYMMDDhhmm
C.8.1 / C.8.202812801 / 04986131doba běhu tarifu T1 / T2 (formát níž)
C.7.1 – C.7.311 / 7 / 162počty výpadků fází
0.9.1 / 0.9.218:43:30 / 26-08-14čas a datum elektroměru
C.6.33,65 Vnapětí záložní baterie

Okamžitý výkon, napětí ani proud tenhle elektroměr neposílá; výkon si Home Assistant dopočítá jako derivaci 1.8.0. Všechno potřebné přijde v prvních 0,18 s, zbytek výpisu je historie.

Problémy s daty

Nejvíc práce mi daly bloky, které na první pohled vypadají v pořádku, ale nejsou.

Optika selhává dvěma způsoby. Při 300 Bd občas chybí nebo je useknutá identifikace; tady pomůže zeptat se znovu. Při 9600 Bd občas vypadne souvislý úsek bloku (dropout), vždy 0,24–0,80 s od začátku. Osm registrů, které firmware používá, přijde do 0,18 s, takže ve všech 45 nahraných vzorcích zůstaly neporušené.

BCC nestačí. Kontrolní součet BCC je jen 7bitový XOR a jednou jím prošel blok, ve kterém chyběla víc než třetina registrů. Proto navíc kontroluji, že platí 1.8.0 == 1.8.1 + 1.8.2 (ve 45 ze 45 vzorků to sedělo s přesností na 1 mWh), že 1.8.0 neklesá a že mezi dvěma čteními nenaroste víc, než je nastavené maximum.

ESPHome počítá ve float32. Při 87 MWh je nejmenší krok float32 asi 7,81 Wh, takže firmware má pro součet tarifů toleranci 0,020 kWh místo 1 mWh v Pythonu.

C.8.x nejsou sekundy, ale hodiny * 100 + minuty. Před odčítáním je proto převádím na minuty, jinak by na přelomu hodiny místo 1 minuty vyšlo 41:

minuty = floor(v / 100) * 60 + (v mod 100)

Hardware

Chtěl jsem použít co nejvíc součástek, které už mám doma, a nic nekupovat. Výsledkem je bastl z optické hlavy s RS485, modulu MAX485, desky LaskaKit ESP32-C3-LPKit v3.2, DC-DC měniče MT3608 a Li-ion článku. Spájené je to dohromady ne moc hezky a místy letmo, ale funguje to.

Zbastlená sestava: ESP32-C3 LPKit, modul MAX485, DC-DC měnič a Li-ion článek
Schéma zapojení: elektroměr, optická hlava, modul MAX485, ESP32-C3 a Home Assistant; Li-ion článek napájí LPKit a přes měnič MT3608 dává 5 V hlavě a modulu Elektroměr ZPA ZE314optický port IEC 62056-21 infračerveně, 7E1, 300 → 9600 Bd Optická hlavanapájení 5 V RS485 A/B Modul MAX485 + CD40695 V, směr přepíná sám TTL UART, dělič 10k/20k na RX LaskaKit ESP32-C3-LPKit v3.2ESPHome WiFi, ESPHome API Home Assistant Měnič MT3608DC-DC na 5 V Li-ion článek 5 V napájení data

Nejvíc jsem řešil napájení, protože u elektroměru není zásuvka. Začal jsem u čtyř alkalických článků D, uvažoval o Li-SOCl2 a 18650 a skončil u Li-ion článku z krabice s bastlením – pozůstatku starého projektu s e-ink displejem.

Na co jsem cestou narazil:

  • Modul MAX485 přepíná směr sám (obvod CD4069 ho odvozuje z TXD), takže ve firmwaru není flow_control_pin.
  • Modul má 5V logiku, ESP32-C3 3,3V. RX proto vede přes dělič 10k/20k, TX přes rezistor 1k.
  • Při prvním spuštění s ESPHome elektroměr mlčel. Značení A+/B- není jednotné, takže první, co zkusit, je prohodit A a B.
  • Brownout resety. Bez kondenzátoru na vstupu DC-DC měniče se ESP32 resetoval. Po přidání 220 µF a připájení země napevno místo přes breadboard resety zmizely a přesnější je i měření napětí baterie.
  • ROM banner. ESP32-C3 po každém probuzení z deep sleepu vypisoval ROM banner na GPIO21, kde je TX k hlavě, takže ho dostal i elektroměr. Teď ho vypínám přes esp_deep_sleep_disable_rom_logging().

Rozvaděč je kovový a se zavřenými dvířky je denní medián signálu WiFi −76 až −78 dBm, nejhůř −82 dBm. Kupodivu to stačí, připojení k WiFi zatím ani jednou nevypršelo. Pro trvalé řešení ale chci anténu vyvést ven.

Firmware v ESPHome

Firmware je ESPHome s komponentou esphome-iec62056 od aquaticus, připnutou na konkrétní otestovaný commit. Jádro konfigurace:

uart:
  id: uart_bus
  tx_pin: GPIO21    # na TXD modulu přes 1k
  rx_pin: GPIO20    # z RXD modulu přes dělič 10k/20k
  baud_rate: 300    # handshake, komponenta pak přepne na 9600
  data_bits: 7
  parity: EVEN
  stop_bits: 1      # flow_control_pin chybí, modul přepíná směr sám

external_components:
  - source: github://aquaticus/esphome-iec62056@994a88edb7121e61f356f3ca4aff5c21179bffb1
    components: [iec62056]

iec62056:
  id: meter_reader
  uart_id: uart_bus
  update_interval: never   # čtení spouští skript po probuzení
  baud_rate_max: 9600
  retry_delay: 4s          # výchozích 15 s je na baterii zbytečně dlouho
  retry_counter_max: 1
  receive_timeout: 3s

sensor:
  - platform: iec62056
    id: raw_total_energy
    obis: "1.8.0"          # přesně jako ve výpisu, bez prefixu 1-0:
    internal: true
  # 1.8.1 a 1.8.2 stejně
  - platform: iec62056
    id: raw_op_time_t1
    obis: "C.8.1"
    internal: true
    filters:
      - lambda: |-
          const float hhmm = roundf(x);
          return floorf(hhmm / 100.0f) * 60.0f + fmodf(hhmm, 100.0f);

Surové hodnoty jsou internal. Do Home Assistantu jdou až přes šablonové senzory, a jen když projdou kontrolou. Jeden cyklus vypadá takto:

  1. Probuzení s vypnutou WiFi, zapnutí napájení hlavy a 250 ms na ustálení.
  2. Čtení, při chybě jeden pokus navíc.
  3. Kontrola 1.8.0, 1.8.1 a 1.8.2. Když něco nesedí, nepublikuje se nic.
  4. WiFi se zapne až po úspěšném čtení: nejvýš 12 s na připojení k síti a 20 s na spojení s Home Assistantem (API).
  5. Publikace všech hodnot najednou.
  6. Deep sleep na 30 minut. Pokud cyklus trvá přes 60 s, failsafe ho ukončí bez publikace.

Komponenta si mezi čteními drží staré hodnoty, takže je firmware před každým čtením zneplatní. Při opakovaném pokusu navíc spadne na 4800 Bd, samotný výpis pak trvá asi 9,7 s a celý cyklus kolem 19 s.

Poslední přijatou hodnotu 1.8.0 si firmware ukládá do NVS (trvalé paměti ve flash), aby mohl hlídat, že stav neklesá. V NVS jsou i čítače výsledků jednotlivých probuzení (publikováno, timeout, zamítnuto, brownout), takže každá mezera v grafu má vysvětlení.

Díky těmto čítačům jsem našel nejzajímavější chybu: probuzení se pravidelně střídala, jedno publikovalo a další skončilo timeoutem API. Knihovna aioesphomeapi, přes kterou se Home Assistant připojuje, zkouší spojení s uspanými zařízeními s odstupem až 15 s. Uzel ale čekal jen 8 s a měl navíc zapnutý úsporný režim WiFi. Po prodloužení na 20 s a vypnutí úsporného režimu uzel přes noc napočítal 61 publikací z 61 probuzení – Home Assistant jich ale dostal jen 60. Uzel totiž publikaci počítal už při přijetí spojení, ještě před šifrovaným handshakem, a za půl sekundy usnul; jestli právě to chybějící snímek způsobilo, prokázané nemám. Teď čeká, až se Home Assistant přihlásí k odběru stavů, a teprve to počítá jako publikaci.

Pro aktualizace přes OTA má uzel přepínač Prevent Deep Sleep, který ho udrží vzhůru. Každá automatická cesta do deep sleepu ho musí respektovat.

Home Assistant

Uzel se do Home Assistantu přidá přes integraci ESPHome. Publikuje 1.8.0, 1.8.1, 1.8.2, aktivní tarif, minuty T1 a T2 za cyklus, čas elektroměru, stav F.F, sílu WiFi signálu a napětí baterie. Home Assistant si poslední hodnoty drží i po dobu, kdy uzel spí.

Na dashboardu mám karty s dnešní spotřebou v T1 a T2:

Home Assistant – karty s dnešní spotřebou v tarifech T1 a T2

Na záložce Energie je jako odběr ze sítě nastavený jen 1.8.0. Tarifní senzory dávají dohromady stejnou energii, takže by se s nimi spotřeba započítala dvakrát.

Home Assistant – záložka Energie s odběrem ze sítě

Čas přepnutí tarifu

Elektroměr neposílá, který tarif právě běží, ani kdy se přepnul. Posílá jen čítače C.8.1 a C.8.2, tedy jak dlouho celkem běžel T1 a T2. V každém okamžiku roste jen jeden z nich, a to i bez odběru, kdy by se energie za hodinu ve float32 nemusela projevit vůbec. Firmware z jejich přírůstku za cyklus publikuje Active Tariff, tedy tarif, který v cyklu běžel déle.

Průběh tarifu v Home Assistantu proto zatím neodpovídá skutečnosti. Příklad z večera 10. října (časy SELČ, čas publikace v HA):

CyklusPřírůstek T1Přírůstek T2Skutečné přepnutíActive Tariff
21:31:47 – 22:01:4613 min17 minT2 → T1 ve 21:48:46T2
22:31:45 – 23:01:4417 min13 minT1 → T2 ve 22:48:44T1

T1 tedy běžel přesně hodinu, ale Home Assistant ho ukáže až od dalšího probuzení ve 22:31 a zpět na T2 přepne až kolem 23:31. Graf tarifu je posunutý o víc než 40 minut.

Z čítačů přitom jde čas přepnutí určit na minutu přesně. V cyklu s přepnutím narostou oba a přepnutí nastalo v čase publikace minus minuty nového tarifu: 22:01:46 − 13 min = 21:48:46. Firmware proto už posílá i T1 Minutes This Cycle a T2 Minutes This Cycle a čas přepnutí se dá z historie dopočítat ručně.

Tarif nepřepínají hodiny elektroměru, ale HDO distributora. Čtyři dny po sobě byl rozpis stejný: T1 v 8:48–9:48, 11:48–12:48, 16:48–17:48 a 21:48–22:48, jinak T2, každé přepnutí do minuty od :48:45. Hodiny elektroměru (0.9.1) jdou asi o 4 minuty napřed, takže v tu chvíli ukazují kolem :52:50.

V plánu je vlastní IEC komponenta, která v cyklu s přepnutím pošle počet minut od přepnutí, a Home Assistant si z času publikace dopočítá přesný čas. Aktuální tarif chci v HA brát z rozpisu HDO od distributora a elektroměr ho bude jen potvrzovat.

Aktuální stav

Uzel běží na baterii v zavřeném rozvaděči a od 11. října jede týdenní test s 30min intervalem. Do 15. října dorazilo do Home Assistantu 190 ze 190 cyklů, celý týden ještě vyhodnocený nemám. V předchozích testech prošlo 60 z 60 cyklů při 10min intervalu a přes noc 24 z 24 při 30min intervalu, bez jediné neplatné hodnoty v Home Assistantu.

Dál chci bastl dotáhnout do trvalé podoby s anténou mimo rozvaděč a upstream komponentu nahradit vlastní. Výdrž baterie zatím neuvádím, změřím ji až na hotové sestavě.