# 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`. ## 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 (`notification_service.send_discord` / `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. Po třech neúspěšných cyklech: přepnutí lokality na **SELF_SUSTAIN** přes `ems.fn_set_mode` (`activated_by` = `system:mismatch`, poznámka = důvod) a **kritický** Discord alert (`notify_self_sustain_activated`). Implementace: `services/control_exporter.py` — `verify_modbus_commands`, `_switch_to_self_sustain`. ## Střídač (Deye) `write_inverter_setpoints` přidá do journalu podle potřeby **62–64** (čas — ne každý export) a **time pointy 148–177** (bloky 3–6 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`. **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 **10 min** a zavolá `verify_modbus_commands`. | ## 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. ## 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` - 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`