solver nastavuje stavy deye
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-20 08:33:56 +02:00
parent 6447666cee
commit 43b594c8d5
10 changed files with 219 additions and 70 deletions

View File

@@ -2,9 +2,10 @@
## Keep it simple
- **Méně heuristik a pevných wattových práhů v řízení** — každá magická konstanta je místo, kde se rodí neshody s plánem a ekonomikou.
- **SELL na Deye** používej jen tam, kde produktově opravdu chceme režim „**vylít baterii do sítě**“ (selling first). Vše ostatní patří do **PASSIVE**: **108/109** dávají **plný proudový rozsah** baterie, směr toku a ekonomiku drží **142** + **TOU** z plánu a instalace.
- Novou logiku vždy ověřit proti **reálnému řádku plánu** (audit / `planning_interval`) a typické chybě „plán říká kW, měnič jede na MW“.
- **Žá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
@@ -16,17 +17,55 @@
| PRESERVE | no_charge, no_discharge | PASSIVE | lock (0/0) |
| MANUAL | solver neběží | EMS nezapisuje | — |
Implementace: omezení LP v `planning_engine.solve_dispatch()` podle `mode_code` z `ems.site_operating_mode`; zápis Deye v `control_exporter.write_inverter_setpoints()` (včetně `lock_battery` u PRESERVE).
Implementace:
## Fyzické režimy Deye (výstup control_exporteru)
- **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.
Detekce v `get_deye_mode` (`battery_w` = `battery_setpoint_w` z plánu, záporné = vybíjení; `grid_setpoint_w` záporné = export do sítě):
### Odkud jsou `battery_setpoint_w` a `grid_setpoint_w` (AUTO)
- **CHARGE:** `battery_w` > 500 **a** `grid_setpoint_w` > 200 → nabíjení ze sítě; reg. **142** dle CHARGE větve v exporteru, **178** = 48.
- **SELL:** `grid_setpoint_w` < 0 **a** `battery_w` < 0 **a** `|battery_w| ≥ |grid_setpoint_w|` → záměr **vybíjet baterii do sítě** (selling first); reg. **142** = 0, **178** = 32, **108** = 0, **109** = max, reg. **143** omezen podle `|grid_setpoint_w|` (viz `control_exporter.py`).
- **PASSIVE:** vše ostatní — **reg. 108** a **109** na **plný strop** z konfigurace střídače (jako SELF_SUSTAIN); jemné řízení výkonu/SOC jde přes **TOU časové body** z plánu, **142** = `deye_zero_export_mode`, **145** = 1, **178** = 48.
Nejde o samostatný „tip“ z predikce FVE, který by exporter náhodně přetáhl do SELL nebo CHARGE.
**SELF_SUSTAIN:** `battery_w = None` ⇒ v `get_deye_mode` jako 0 ⇒ typicky **PASSIVE**; v `write_inverter_setpoints` při `self_sustain_local_use=True`**108 i 109 = max**, reg. **142** = zero export dle DB, TOU SOC = **`min_soc_percent`**. **PRESERVE:** `lock_battery`**108 = 0**, **109 = 0**.
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)