Files
ems/docs/04-modules/operating-modes.md
Dusan Vojacek 43b594c8d5
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped
solver nastavuje stavy deye
2026-04-20 08:33:56 +02:00

111 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).
- **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 | no_export, min_import | 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** |
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í*).
**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`
- **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`)