Files
ems/docs/04-modules/modbus-command-journal.md
Dusan Vojacek dede8d604d
Some checks failed
CI and deploy / migration-check (push) Failing after 13s
CI and deploy / deploy (push) Has been skipped
fix cutoff a grid peak shaving register
2026-04-29 13:36:38 +02:00

9.1 KiB
Raw Permalink Blame History

Modbus command journal

Účel

Každý zápis na Modbus TCP (Deye a později další aktiva) se ukládá do tabulky ems.modbus_command jako samostatný řádek: cílový registr, hodnota, endpoint, vazba na site_id a volitelně na planning_run_id. Po zápisu má řádek stav written; samostatný verifikační job (každé 2 minuty) nebo ruční GET /api/v1/sites/{site_id}/control/verify přečte registr zpět a nastaví value_verified a stav verified nebo mismatch. Výjimka: Deye 6264 (systémový čas) se vždy ověřují jako celek jedním čtením 6264 a tolerančně podle dekódovaného data/času — řádky 6264 se neprohánějí striktní větví po jednom registru (jinak by zejména 64 způsoboval falešné mismatch a SELF_SUSTAIN). Podmnožina written řádků (např. jen 64) se sloučí s dotazem na všechny written 6264 pro daný invertor; viz modbus-registers.md.

Schéma ems.modbus_command

Sloupec Význam
asset_type / asset_id / asset_code Typ aktiva (inverter, …), FK logicky na příslušnou tabulku, čitelný kód
device_* Host, port, Modbus unit ID
register Číslo registru (decimal); v logu též hex
register_name Např. charge_limit, export_limit
value_to_write / value_written / value_verified Požadavek, potvrzený zápis, ověření čtením
status pending, written, verified, failed, mismatch, retrying
planning_run_id Volitelná vazba na aktivní plán
deye_physical_mode U zápisů z write_inverter_setpoints: PASSIVE / SELL / CHARGE (stejná hodnota na všech řádcích daného běhu exportu); jinak NULL
attempt_count Počet pokusů o zápis (pro limity retry)

Indexy: podle (site_id, status, created_at) a částečný index pro pending / retrying.

Verifikace a bezpečnost

  1. Po mismatch se odešle Discord alert (notify_modbus_mismatch), pokud je nastaven DISCORD_WEBHOOK_URL.
  2. Retry zápisu max. 3× (počítáno přes attempt_count po zápisech).
  3. Reg 178 (grid peak shaving switch): journal ukládá celé 16bit value_to_write (32 nebo 48). Při ověření se za shodu považuje shoda bitů 45 maskou 0x0030 s očekáváním; value_verified = přečtená surová hodnota. Při nesouladu masky se jednou znovu přečte reg. 178 (druhé FC3) kvůli glitchům na RS485 — pokud druhé čtení maskou sedí, stav je verified.
  4. Reg 178 (control board special 1, BA81 GEN cut-off): exporter nastavuje bits 01 (2/3) pomocí read-modify-write, protože reg 178 je bitové pole i pro další volby (např. peak shaving bits 45). Při ověření se za shodu považuje maska bits 01 a 45 (0x0033) vůči očekávání.
  5. TOU výkon W (154159): firmware často vrátí max. výkon z reg. 108/109 × 51.2 V místo přesně zapsaného W; verify to akceptuje jako shodu (skutečný výkon je stejně omezen proudy 108/109).
  6. Pojistka 6264: pokud by se řádek registru 62, 63 nebo 64 omylem dostal do striktní větve po jednom registru, verify to zachytí a zpracuje jako toleranční celek 6264 (stejně jako primární clock větev) — bez přepnutí do SELF_SUSTAIN jen kvůli tomu.
  7. Po třech neúspěšných cyklech ověření:
    • Kritické Deye registry (108, 109, 142, 143, 145): přepnutí lokality na SELF_SUSTAIN přes run_fn_set_mode_with_discordems.fn_set_mode (activated_by = system:mismatch, poznámka = důvod). Při skutečné změně mode_code jde na Discord kritická zpráva (stejný formát jako u ostatních přepnutí režimu).
    • Nekritické / soft registry (např. 178 po vyčerpání druhého čtení, 154159 bez akceptovaného clampu, ostatní mimo výše uvedené kritické): po 3 pokusech zůstane řádek v mismatch, jde Discord (notify_modbus_mismatch), režim se nemění.
    • Výjimka — systémový čas 6264: přepnutí režimu se neprovádí. Po 3 neúspěšných ověřeních jde kritický Discord (notify_modbus_clock_verify_exhausted); střídač a EMS režim zůstávají v aktuálním stavu (čas na sběrnici může vyžadovat ruční kontrolu / firmware).

Po návratu SELF_SUSTAIN → AUTO (přes fn_set_mode): notification_service naplánuje na pozadí rolling replan (run_plan_api, triggered_by=mode:self_sustain_exit), aby aktivní plán odpovídal znovu plné optimalizaci v AUTO.

Baseline po deployi (operativa): např. počet přepnutí na SELF_SUSTAIN z verify za poslední 2 dny:
SELECT count(*) FROM ems.site_operating_mode_log WHERE mode_code = 'SELF_SUSTAIN' AND activated_by = 'system:mismatch' AND activated_at >= now() - interval '2 days';
Pro diagnostiku času Deye po opravě clock logiky používej u modbus_command krátké okno (např. verified_at >= now() - interval '2 days').

Discord při jakékoli změně režimu (nejen Modbus): notification_service.run_fn_set_mode_with_discord volá ems.fn_set_mode a při změně mode_code oproti stavu před voláním pošle zprávu (notify_operating_mode_changed). Úroveň: user:api → info, obecné system:* → warning, system:mismatch → critical. Použití: HTTP POST /api/v1/sites/{site_id}/mode, _switch_to_self_sustain v control_exporter. Vypršení valid_until: ems.fn_expire_modes() vrací řádky (site_id, site_code, old_mode, new_mode) pro každé provedené přepnutí; scheduler v main.py (a lazy expire v _fetch_operating_mode) z nich pošle Discord.

Implementace: services/control_exporter.pyverify_modbus_commands, _verify_deye_clock_written_bundle, _fetch_written_deye_clock_commands, _switch_to_self_sustain, DEYE_CRITICAL_REGS_SELF_SUSTAIN, _deye_tou_power_verify_match; services/notification_service.pyrun_fn_set_mode_with_discord, _auto_rolling_replan_after_self_sustain_exit, notify_operating_mode_changed.

Střídač (Deye)

write_inverter_setpoints přidá do journalu podle potřeby 6264 (čas — po čtení z invertoru jen při driftu / 24h intervalu; viz modbus-registers.md) a time pointy 148177 (bloky 36 typicky jednou denně; viz modbus-registers.md), dále 108, 109, 141, 142, 178, 143. Každý řádek daného exportního běhu má deye_physical_mode (PASSIVE / SELL / CHARGE). Reg 191 EMS nezapisuje (SolarmanApp). Převod výkonu: battery_watts_to_amps v modbus-registers.md.

Pokud je zapnutý feature asset_inverter.deye_gen_microinverter_cutoff_enabled = true, exporter nastavuje MI export cutoff přes reg 178 bits01 (BA81 GEN port cut-off) — stále jako jeden záznam modbus_command pro reg 178 (spolu s peak shaving bity 45).

Dávky: execute_modbus_commands slučuje souvislé adresy do jednoho write_registers (FC 0x10). verify_modbus_commands čte zpět po souvislých blocích (read_holding_registers, FC 0x03). Detail režimů: modbus-registers.md.

APScheduler

Job Frekvence Popis
verify_modbus každé 2 min Pro každou aktivní site vybere written příkazy s written_at v posledních 20 min a zavolá verify_modbus_commands.
plan_actual_slot_guard :05, :20, :35, :50 (po audit_filler) ems.fn_plan_actual_slot_guard_all_active (+ plan_actual_slot_guard.py jen Discord): poslední 2 uzavřené 15min sloty — fatální odchylka plán vs. audit síťDiscord (critical), dedup přes ems.plan_fatal_deviation_sent.

Plná tabulka jobů je v lifespan.py.

Ruční API

GET /api/v1/sites/{site_id}/control/verify?minutes=10

Vrátí počty checked / verified / mismatch a seznam dotčených příkazů s aktuálním stavem po verifikaci.

ems.cutoff_switch_log

Tabulka pro budoucí logování cut-off přepínačů (mikroinvertory / GEN při záporné prodejní ceně). Záznam při změně stavu: asset_code, new_state, previous_state, reason, sell_price_czk, triggered_by. Zatím jen schéma; logika napojení v control_exporter je v TODO.

Poznámka: GEN port cut-off na BA81 se aktuálně provádí přímo přes Deye reg 178 (bits01) a loguje se v ems.modbus_command. cutoff_switch_log je oddělená tabulka pro budoucí obecnější “cut-off” akce (nezávisle na konkrétním Modbus registru).

Konfigurace

  • .env: DISCORD_WEBHOOK_URL — prázdné = notifikace vypnuté (jen log).

Související soubory

  • Migrace: db/migration/V023__modbus_command_journal.sql, V025__deye_physical_mode.sql, V030__deye_clock_sync_at.sql, V044__deye_register_max_current_a.sql; repeatables db/routines/R__044_fn_set_mode.sql (fn_expire_modes vrací detail přepnutí pro notifikace)
  • Backend: backend/services/control_exporter.py, backend/services/modbus_client.py, backend/services/notification_service.py, backend/app/main.py
  • Registry Deye: docs/04-modules/modbus-registers.md