# Provozní režimy EMS ## Keep it simple - **Žádné wattové prahy pro výběr SELL / CHARGE** — mapování z MILP setpointů je čistě ze **znamének** `battery_setpoint_w` a `grid_setpoint_w` (viz `get_deye_mode` v `exporter_monolith.py`). - **Přetok FVE do sítě** se neodvozuje z forecastového capu: plán nese explicitní `export_limit_w` jako tvrdý limit lokality / invertoru, ne jako tipované maximum z předpovědi. - **ZERO (PASSIVE)** = zero export k CT/zátěži (**142** = `deye_zero_export_mode`), s **plnými proudy 108/109** jen ve výchozím stavu; pro přetok FVE do sítě nebo odběr ze sítě bez vybíjení baterie se jeden z proudů **vynuluje** (`_deye_zero_export_amps_for_passive`). - **SELL** = plánovaný export **i** plánované vybíjení (oba záporné) → **142** = selling first, **178** = vypnutý grid peak shaving (32); po návratu do ZERO/CHARGE zase **178** = 48. - Novou logiku vždy ověřit proti **reálnému řádku plánu** (audit / `planning_interval`). ## Přehled | Mode | Solver constraints | Deye fyzický režim | Baterie | |------|-------------------|-------------------|---------| | AUTO | žádné | PASSIVE/SELL/CHARGE dle plánu | dle plánu | | SELF_SUSTAIN | min_import; export jen jako nouzový ventil (silně penalizovaný) | vždy PASSIVE | plné limity | | CHARGE_CHEAP | no_export, no_discharge | CHARGE | nabíjení max | | PRESERVE | no_charge, no_discharge | PASSIVE | lock (0/0) | | MANUAL | solver neběží | EMS nezapisuje | — | Implementace: - **EMS provozní režim** (`AUTO`, `SELF_SUSTAIN`, …): jediný zdroj v DB `ems.site_operating_mode.mode_code` + větev v `_build_setpoints` / `export_setpoints` (např. `CHARGE_CHEAP` přepíše setpointy před zápisem — stále jedna funkce exportu). - **Deye fyzický režim** (`PASSIVE` / `CHARGE` / `SELL`): jediný zdroj **`get_deye_mode`** (`exporter_monolith.py`); zápis v `write_inverter_setpoints()`. - Omezení LP v `planning_engine.solve_dispatch()` podle `mode_code`; zápis Deye včetně `lock_battery` u PRESERVE. ### Odkud jsou `battery_setpoint_w` a `grid_setpoint_w` (AUTO) Nejde o samostatný „tip“ z predikce FVE, který by exporter náhodně přetáhl do SELL nebo CHARGE. 1. **Zdroj dat:** pro režim **AUTO** exporter načte aktivní řádek **`ems.planning_interval`** pro aktuální 15min slot (`_fetch_plan_row_for_slot_offset` → `_build_setpoints` v `exporter_monolith.py`). 2. **Kdo je naplnil:** sloupce pocházejí z výstupu **`planning_engine.solve_dispatch()`** — MILP nad bilanční rovnicí za slot (základní značky v kódu: `gi[t]` ≥ 0 import ze sítě, `ge[t]` ≥ 0 export ze sítě, `bc[t]` / `bd[t]` nabíjení / vybíjení baterie). Uložené hodnoty odpovídají **`grid_setpoint_w = round(gi[t] - ge[t])`** a **`battery_setpoint_w = round(bc[t] - bd[t])`** (viz sestavení `DispatchResult` a zápis plánu). 3. **Fyzika u elektroměru:** v jednom slotu model pracuje s **čistým** tokem ze sítě jako rozdílem `gi` a `ge`; predikce PV a spotřeba vstupují do **bilance a omezení** řešiče, ne jako náhradní logika mapování na Deye. 4. **Role `get_deye_mode`:** pouze **přeloží** už hotový plán na kombinaci registrů (PASSIVE / CHARGE / SELL). Očekávání provozu (např. kdy přesně má být výdej baterie do sítě vs. přetok FVE) má držet **LP a výběr slotů** (`allow_charge`, `allow_discharge_export`, …), ne dodatečné wattové heuristiky v exporteru. ## Fyzické režimy Deye (výstup control exporteru) **Jediné místo** pro klasifikaci **Deye** `PASSIVE` | `CHARGE` | `SELL` z MILP setpointů je **`get_deye_mode`** v `exporter_monolith.py`. Značení: `battery_w` = `battery_setpoint_w` (kladné = nabíjení, záporné = vybíjení); `grid_setpoint_w` (kladné = import, záporné = export). | Režim | Podmínka z plánu | 108 / 109 (zkráceně) | 142 | 178 | |--------|------------------|----------------------|-----|-----| | **CHARGE** | `battery_w` > 0 **a** `grid_setpoint_w` > 0 | dle plánu nabíjení / 0 vybíjení | větev CHARGE | 48 | | **SELL** | `battery_w` < 0 **a** `grid_setpoint_w` < 0 | 0 nabíjení / max vybíjení | 0 (selling first) | **32** (peak shaving off) | | **PASSIVE (ZERO)** | vše ostatní | viz tabulka ZERO níže | `deye_zero_export_mode` | 48 | ### ZERO: výchozí a dvě varianty proudu (reg. 108 / 109) Všechny řádky předpokládají **142** = zero export (ne SELL). | Situace | Podmínka (plán) | Reg. 108 (max charge A) | Reg. 109 (max discharge A) | |---------|-----------------|-------------------------|----------------------------| | Výchozí | ostatní případy PASSIVE | max | max | | Přetok FVE do sítě | `grid_setpoint_w` < 0 **a** `battery_w` ≥ 0 | **0** | max | | Držet baterii, brát ze sítě | `grid_setpoint_w` > 0 **a** `battery_w` ≤ 0 | max | **0** | V obou exportních případech platí, že `export_limit_w` je **tvrdý site/inverter cap**. Nejde o forecastový odhad exportu, takže se může pustit plný přetok v rámci distribučního limitu. Nabíjení ze sítě s vysokým cílovým SoC v TOU řeší větev **CHARGE** (grid charge v time pointech), ne tato tabulka. ### ZERO a „zakázaný export FVE do sítě“ (jen řiditelná pole) **Reg. 145 (solar sell)** na Deye je přepínač typu **enabled / disabled** pro **přebytek FVE na straně měniče** (počítá se vůči režimu **142** zero export a stavu **108** — viz `modbus-registers.md`, pass-through krok za krokem). - **Pouze to, co EMS umí přes Deye Modbus ovlivnit** — typicky **FVE pole řízené střídačem** (`asset_pv_array.controllable = true`, u referenční lokality **home-01** např. string za Deye). - **Pole mimo tento kanál** (např. **pv-b** u **home-01**, `controllable = false`, často samostatný ongrid GEN) **reg. 145 neovládá**; jejich výkon jde do plánovače jako `pv_b_forecast_w`, ale curtailment / solar sell logika Deye se jich netýká. **Implementace dnes:** exporter vždy zapisuje **145 = 1** (solar sell enabled). Tvrdé vypnutí přebytku řiditelného FVE do sítě přes **145 = 0** z politik (`no_export`, `BLOCK_EXPORT`, …) je v plánu — viz **`docs/05-todo.md`** (sekce *Deye řízení – rozšíření*). **Implementace (BLOCK_EXPORT):** při `effective_sell_price < 0` (slot z plánu) EMS drží fyzicky stále **PASSIVE**, ale zapne **zákaz exportu přebytků** pro řiditelnou FVE: - **reg 145 = 0** (solar sell disabled) mimo SELL - **BA81:** navíc přes **reg 178** (bits0–1; v některých UI jako “register 179”) aktivuje „MI export to Grid cutoff“ pro mikroinvertory na GEN portu (jen pokud je `asset_inverter.deye_gen_microinverter_cutoff_enabled = true`). Týká se jen výroby, kterou Deye umí ovlivnit; **pv-b / ongrid GEN** u home-01 tímto neomezíš. #### PV1/PV2 vs. GEN port (důležité pro BLOCK_EXPORT) - **PV1/PV2 (hlavní stringy na DC vstupu Deye)**: výkon je v režimu zero-export **řiditelný** (střídač umí výrobu stáhnout až k nule, pokud není odběr a baterie už nemůže nabíjet). Při BLOCK_EXPORT tedy dává smysl „zakázat export“ přes **reg 145 = 0** – Deye zamezí přetokům z těchto stringů.\n+- **GEN port (AC coupling / mikroinvertory / ongrid GEN)**: výkon **nelze plynule omezovat**. Pole vyrábí „co dá slunce“ a pokud ho **nespotřebuje dům + EV/TČ + baterie**, přebytek fyzicky teče do sítě.\n+ - U instalací typu **BA81** je proto k dispozici jen **tvrdý cut-off** (reg 179 bits0–1).\n+ - U **malé baterie** (např. BA81 ~6 kW max charge a navíc při vysokém SoC ještě méně) může při plném osvitu často nastat přebytek i při BLOCK_EXPORT – a bez cut-off by šel do sítě.\n+ - Naopak při **malém osvitu / velké spotřebě** jsou „každé watty z GEN“ užitečné (jít do domu/baterie) a cut-off by zbytečně zahodil výrobu.\n+ Z toho plyne: **cut-off GEN portu je smysluplné řídit podle očekávaného přebytku**, ne jen podle „sell < 0“. Detail návrhu implementace je v `docs/04-modules/planning.md` (sekce o GEN portu a export banu). **SELF_SUSTAIN:** `battery_w = None` ⇒ v `get_deye_mode` jako 0 ⇒ **PASSIVE**; v `write_inverter_setpoints` při `self_sustain_local_use=True` → **108 i 109 = max** (bez variant ZERO výše), reg. **142** dle DB, TOU SOC = **`min_soc_percent`**. **PRESERVE:** `lock_battery` → **108 = 0**, **109 = 0**. ## EMS politiky (nejsou fyzické stavy Deye) - **PV_SELL_ONLY:** AUTO + constraint solveru `max_discharge_from_pv` - **BLOCK_EXPORT:** AUTO + záporná sell_price → `ge[t]=0` (hard constraint, pokud má lokalita k dispozici GEN port cut-off; jinak solver export jen penalizuje přes zápornou cenu) - **NEGATIVE_HARVEST:** AUTO + záporná buy_price → max charge/load - **PROTECT:** SELF_SUSTAIN s konzervativními limity Tyto politiky jsou parametrizace AUTO/SELF_SUSTAIN, ne samostatné fyzické stavy. --- ## Loxone a UI (shrnutí) EMS a Loxone sdílí pojmenované provozní režimy; Loxone dostává číslo režimu přes Virtual Input a může fungovat autonomně (watchdog při výpadku EMS). ``` POST /api/v1/sites/{site_id}/mode { "mode": "SELF_SUSTAIN", "valid_until": null, "notes": "…" } ``` Backend: `ems.fn_set_mode` přes `run_fn_set_mode_with_discord` (při skutečné změně `mode_code` → Discord, pokud je `DISCORD_WEBHOOK_URL`) + HTTP na Loxone `/dev/sps/io/EMS_Mode/{loxone_mode_value}`. Dočasné přepisy s `valid_until` ruší `ems.fn_expire_modes()`, která vrací řádky `(site_id, site_code, old_mode, new_mode)` pro každé přepnutí — scheduler je použije pro stejné Discord upozornění. **Klíčový princip:** Loxone watchdog nečte DB – sleduje pulzy `EMS_Heartbeat`. Detail: `docs/loxone-integration.md`. Detail Modbus / Discord: `docs/04-modules/modbus-command-journal.md`. ### Tabulka režimů (Loxone / zátěže) | Kód | Loxone int | EV | TČ | Poznámka | |-----|------------|----|----|----------| | `AUTO` | 1 | dle plánu | dle plánu | setpointy z plánu | | `SELF_SUSTAIN` | 2 | stop | stop | fallback / výpadek EMS | | `CHARGE_CHEAP` | 3 | stop | stop | max nabíjení ze sítě | | `PRESERVE` | 4 | stop | stop | baterie uzamčena (Modbus 0/0) | | `MANUAL` | 0 | stop | stop | servis, EMS neexportuje | ### Otevřené body - [ ] Doplnit alerty při `ems_heartbeat_status = 'stale'` (Discord při změně provozního režimu z backendu je popsán v `modbus-command-journal.md`)