fix battery charge u self_sustain rezimu
All checks were successful
CI and deploy / migration-check (push) Successful in 3s
CI and deploy / deploy (push) Successful in 25s

This commit is contained in:
Dusan Vojacek
2026-04-19 15:09:33 +02:00
parent 5c868083af
commit efc2cbfded
5 changed files with 51 additions and 4 deletions

View File

@@ -308,6 +308,8 @@ class ControlSetpoints:
effective_sell_price_czk_kwh: float | None = None effective_sell_price_czk_kwh: float | None = None
#: True = reg 108/109 na 0 (PRESERVE Deye baterii nepoužívá) #: True = reg 108/109 na 0 (PRESERVE Deye baterii nepoužívá)
lock_battery: bool = False lock_battery: bool = False
#: Režim SELF_SUSTAIN: plný rozsah nabíjení/vybíjení na invertoru + zero-export (reg 142) a nízké TOU %.
self_sustain_local_use: bool = False
@dataclass @dataclass
@@ -1184,6 +1186,7 @@ def _build_setpoints(mode: OperatingModeInfo, pi: asyncpg.Record | None) -> Cont
ev1_power_w=0, ev1_power_w=0,
ev2_power_w=0, ev2_power_w=0,
target_soc_pct=None, target_soc_pct=None,
self_sustain_local_use=True,
) )
if code == "CHARGE_CHEAP": if code == "CHARGE_CHEAP":
@@ -1291,8 +1294,14 @@ def _deye_passive_tou_battery_soc_pct(
Jinak zůstane provozní podlaha ``min_soc_percent`` (typicky nízká % → přetok do sítě Jinak zůstane provozní podlaha ``min_soc_percent`` (typicky nízká % → přetok do sítě
možný dle chování Deye). možný dle chování Deye).
Režim **SELF_SUSTAIN** (``self_sustain_local_use``): vždy ``min_soc_percent`` — nízké
TOU drží prioritu „baterie jako buffer“ při plném reg. 108/109 a reg. 142 zero-export;
neaplikuje se sem logika 100 % podle ceny (LP se v SELF_SUSTAIN nepoužívá).
""" """
mn = _deye_tou_min_soc_pct(inv) mn = _deye_tou_min_soc_pct(inv)
if setpoints.self_sustain_local_use:
return mn
bat_w = 0 if setpoints.battery_w is None else int(setpoints.battery_w) bat_w = 0 if setpoints.battery_w is None else int(setpoints.battery_w)
sell = setpoints.effective_sell_price_czk_kwh sell = setpoints.effective_sell_price_czk_kwh
@@ -1311,7 +1320,8 @@ def get_deye_mode(setpoints: ControlSetpoints) -> str:
SELL only when battery actively discharges for grid export (bat_w < -500 SELL only when battery actively discharges for grid export (bat_w < -500
AND grid_w < -200). Pass-through (PV → grid, battery idle) stays PASSIVE AND grid_w < -200). Pass-through (PV → grid, battery idle) stays PASSIVE
with reg 108 = 0 + reg 145 = 1 (solar sell). with reg 108 = 0 + reg 145 = 1 (solar sell).
battery_w=None (SELF_SUSTAIN) → bat_w considered 0 → PASSIVE. battery_w=None (SELF_SUSTAIN) → bat_w considered 0 → PASSIVE; při exportu se ale
zapíše plný reg. 108/109 (viz ``self_sustain_local_use`` v ``write_inverter_setpoints``).
""" """
grid_w = int(setpoints.grid_setpoint_w or 0) grid_w = int(setpoints.grid_setpoint_w or 0)
bat_w = 0 if setpoints.battery_w is None else int(setpoints.battery_w) bat_w = 0 if setpoints.battery_w is None else int(setpoints.battery_w)
@@ -1384,6 +1394,11 @@ async def write_inverter_setpoints(
elif deye_mode == "CHARGE": elif deye_mode == "CHARGE":
charge_a = battery_watts_to_amps(bat_w, inv.max_charge_a) charge_a = battery_watts_to_amps(bat_w, inv.max_charge_a)
discharge_a = 0 discharge_a = 0
elif setpoints_now.self_sustain_local_use:
# SELF_SUSTAIN: plný nabíjecí i vybíjecí proud invertoru — přebytek FVE jde do baterie,
# reg. 142 = zero export to load/CT (viz selling_mode níže), ne reg. 108 = 0.
charge_a = int(inv.max_charge_a)
discharge_a = int(inv.max_discharge_a)
else: else:
charge_a = int(inv.max_charge_a) if bat_w > 0 else 0 charge_a = int(inv.max_charge_a) if bat_w > 0 else 0
discharge_a = int(inv.max_discharge_a) discharge_a = int(inv.max_discharge_a)

View File

@@ -123,6 +123,26 @@ class DeyeTouParamsTests(unittest.TestCase):
self.assertTrue(g) self.assertTrue(g)
self.assertEqual(s, 95) self.assertEqual(s, 95)
def test_self_sustain_tou_stays_min_soc_even_if_sell_negative(self) -> None:
"""SELF_SUSTAIN: nízké TOU (min_soc), ne 100 % z negativní vykupní — LP se nepoužívá."""
sp = ControlSetpoints(
battery_w=None,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=0,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=None,
effective_sell_price_czk_kwh=-0.48,
self_sustain_local_use=True,
)
self.assertEqual(get_deye_mode(sp), "PASSIVE")
_p, s, g = _deye_tou_params(sp, _inv(min_soc=12, reserve_soc=20))
self.assertFalse(g)
self.assertEqual(s, 12)
def test_lock_battery_uses_min_soc(self) -> None: def test_lock_battery_uses_min_soc(self) -> None:
sp = ControlSetpoints( sp = ControlSetpoints(
battery_w=0, battery_w=0,

View File

@@ -121,6 +121,8 @@ Solver rozlišuje **čtyři typy slotů**: **Charge**, **Pass-through**, **Disch
**Pass-through** (PV → síť, baterie idle) zůstává **PASSIVE** — fyzicky se realizuje nastavením reg 108 = 0 (zákaz nabíjení) + reg 145 = 1 (solar sell), takže PV přebytky tečou do sítě. **Pass-through** (PV → síť, baterie idle) zůstává **PASSIVE** — fyzicky se realizuje nastavením reg 108 = 0 (zákaz nabíjení) + reg 145 = 1 (solar sell), takže PV přebytky tečou do sítě.
**SELF_SUSTAIN** (záložní režim po Modbus mismatch apod.) zůstává **PASSIVE** z hlediska `get_deye_mode`, ale `write_inverter_setpoints` nastaví **reg. 108 i 109 na maximum z DB** (`self_sustain_local_use=True` v `ControlSetpoints`), **reg. 142** na `asset_inverter.deye_zero_export_mode` (1 = zero export to load, 2 = zero export to CT) a **TOU SOC** na **`min_soc_percent`** (typicky 12 %), aby střídač maximalizoval využití baterie lokálně místo zákazu nabíjení při `battery_w=None`.
### Klíčové registry podle typu slotu ### Klíčové registry podle typu slotu
| Registr | Charge | Pass-through | Discharge-export | Self-consumption | | Registr | Charge | Pass-through | Discharge-export | Self-consumption |
@@ -131,6 +133,8 @@ Solver rozlišuje **čtyři typy slotů**: **Charge**, **Pass-through**, **Disch
| **145** (solar sell) | 1 | 1 | 1 | 1 | | **145** (solar sell) | 1 | 1 | 1 | 1 |
| **178** (peak shaving) | 48 | 48 | **32** | 48 | | **178** (peak shaving) | 48 | 48 | **32** | 48 |
Při provozním režimu **SELF_SUSTAIN** je v PASSIVE **108 = max** i **109 = max** (viz odstavec výše), nikoli hodnoty ve sloupci „Pass-through“.
Hodnota `deye_zero_export_mode` (1 = zero export to load, 2 = zero export to CT) pochází z `asset_inverter.deye_zero_export_mode` a závisí na fyzické instalaci (přítomnost CT). Detail v [`modbus-registers.md`](modbus-registers.md). Hodnota `deye_zero_export_mode` (1 = zero export to load, 2 = zero export to CT) pochází z `asset_inverter.deye_zero_export_mode` a závisí na fyzické instalaci (přítomnost CT). Detail v [`modbus-registers.md`](modbus-registers.md).
**TOU (time points, reg. 166+):** SOC závisí na fyzickém režimu z `get_deye_mode`**SELL** zapisuje ekonomickou rezervu (`reserve_soc_percent`), **PASSIVE** a neaktivní řádky **36** provozní minimum (`min_soc_percent`). Viz [`modbus-registers.md`](modbus-registers.md). **TOU (time points, reg. 166+):** SOC závisí na fyzickém režimu z `get_deye_mode`**SELL** zapisuje ekonomickou rezervu (`reserve_soc_percent`), **PASSIVE** a neaktivní řádky **36** provozní minimum (`min_soc_percent`). Viz [`modbus-registers.md`](modbus-registers.md).

View File

@@ -58,6 +58,14 @@ Režim **CHARGE_CHEAP** v EMS nastaví `grid_setpoint_w` tak, aby platila podmí
**Důležité:** SELL se aktivuje **pouze** při záměrném vybíjení baterie do sítě (`bat_w < 500`). Pass-through (PV → síť, baterie idle) zůstává v PASSIVE s reg 108 = 0. **Důležité:** SELL se aktivuje **pouze** při záměrném vybíjení baterie do sítě (`bat_w < 500`). Pass-through (PV → síť, baterie idle) zůstává v PASSIVE s reg 108 = 0.
### Provozní režim EMS SELF_SUSTAIN
Z hlediska `get_deye_mode` je **SELF_SUSTAIN** stále **PASSIVE** (`battery_w` z LP je `None`). Exportér ale nastaví `ControlSetpoints.self_sustain_local_use=True` a v `write_inverter_setpoints`:
- **108 / 109** = **max** z invertoru (DB) — plný rozsah nabíjení i vybíjení, aby přebytek FVE mohl do baterie.
- **142** = `asset_inverter.deye_zero_export_mode` (**1** = zero export to load, **2** = zero export to CT), stejně jako u ostatního PASSIVE mimo SELL.
- **TOU SOC** (reg 166+) = vždy **`min_soc_percent`** (typicky 12 %) — `_deye_passive_tou_battery_soc_pct` při tomto příznaku **ne** přepíná na 100 % podle vykupní ceny, protože LP se v SELF_SUSTAIN nepoužívá.
### Čtyři typy slotů a mapování na registry ### Čtyři typy slotů a mapování na registry
Solver předvybírá sloty pro nabíjení a export-vybíjení (`_select_charge_slots`, `_select_discharge_export_slots`). Nabíjení: vždy povoleno v slotech s PV-surplus; zbytek rozpočtu (`charge_slot_buffer × (soc_max current_soc) PV přínos`) doplněn nejlevnějšími sloty podle **`buy_price`** (nákupní cena ze sítě). Export-vybíjení: top-N slotů podle nejvyšší **`sell_price`**. Výsledné setpointy pak určují typ slotu: Solver předvybírá sloty pro nabíjení a export-vybíjení (`_select_charge_slots`, `_select_discharge_export_slots`). Nabíjení: vždy povoleno v slotech s PV-surplus; zbytek rozpočtu (`charge_slot_buffer × (soc_max current_soc) PV přínos`) doplněn nejlevnějšími sloty podle **`buy_price`** (nákupní cena ze sítě). Export-vybíjení: top-N slotů podle nejvyšší **`sell_price`**. Výsledné setpointy pak určují typ slotu:
@@ -75,7 +83,7 @@ Solver předvybírá sloty pro nabíjení a export-vybíjení (`_select_charge_s
| **141** energy mode | 0 | 0 | 0 | 0 | | **141** energy mode | 0 | 0 | 0 | 0 |
| **TOU SOC** (reg 166+) | viz níže | min_soc_pct | reserve_soc_pct | min_soc_pct | | **TOU SOC** (reg 166+) | viz níže | min_soc_pct | reserve_soc_pct | min_soc_pct |
**PASSIVE TOU SOC % (home-01 / Deye):** EMS ukládá do řádku time pointu procento, které na zařízení řídí **prioritu baterie vs. přetok FVE do sítě** (viz firmware / instalace). Je-li zapsané procento **níž než skutečný SoC**, přebytek tíhne do sítě; při **záporné efektivní vykupní** (`effective_sell_price` ze slotu) nebo při **kladném `battery_setpoint_w`** (plánované nabíjení) EMS nastaví **100 %** (signál „využij baterii naplno“). **`asset_battery.max_soc_percent`** (typicky 95) je **jiný účel**: horní limit pro **plánovač / denní provoz v % SoC** (komfort, degradace, rezerva výrobce), **ne** časové „do kdy“ ani hodnota zapisovaná do tohoto TOU při této priorité. Jinak zůstane **`min_soc_percent`**. **PASSIVE TOU SOC % (home-01 / Deye):** EMS ukládá do řádku time pointu procento, které na zařízení řídí **prioritu baterie vs. přetok FVE do sítě** (viz firmware / instalace). Je-li zapsané procento **níž než skutečný SoC**, přebytek tíhne do sítě; při **záporné efektivní vykupní** (`effective_sell_price` ze slotu) nebo při **kladném `battery_setpoint_w`** (plánované nabíjení) EMS nastaví **100 %** (signál „využij baterii naplno“)**ne** v režimu **SELF_SUSTAIN** (`self_sustain_local_use`), tam je vždy **`min_soc_percent`**. **`asset_battery.max_soc_percent`** (typicky 95) je **jiný účel**: horní limit pro **plánovač / denní provoz v % SoC** (komfort, degradace, rezerva výrobce), **ne** časové „do kdy“ ani hodnota zapisovaná do tohoto TOU při této priorité. Jinak zůstane **`min_soc_percent`**.
**Jak funguje pass-through fyzicky:** **Jak funguje pass-through fyzicky:**

View File

@@ -16,11 +16,11 @@ Implementace: omezení LP v `planning_engine.solve_dispatch()` podle `mode_code`
Detekce z `battery_w` a `grid_setpoint_w` (`get_deye_mode`): Detekce z `battery_w` a `grid_setpoint_w` (`get_deye_mode`):
- **PASSIVE:** `grid_setpoint_w >= -200` → reg142=1, reg178=48, 108/109=max z DB (nebo 0/0 při `lock_battery`) - **PASSIVE:** `grid_setpoint_w >= -200` → reg **142** = `deye_zero_export_mode`, reg **178** = 48; **108** = max jen při plánovaném nabíjení (`battery_w` > 0), jinak typicky **108 = 0** a **109** = max — **výjimka SELF_SUSTAIN:** příznak `self_sustain_local_use`**108 i 109 = max** (viz `control_exporter.py`). **0/0** jen při `lock_battery` (PRESERVE).
- **SELL:** `grid_setpoint_w < -200` → reg142=0, reg178=32, 108/109=max - **SELL:** `grid_setpoint_w < -200` → reg142=0, reg178=32, 108/109=max
- **CHARGE:** `grid_setpoint_w > 200` **a** `battery_w > 500` → reg142=1, reg178=48 - **CHARGE:** `grid_setpoint_w > 200` **a** `battery_w > 500` → reg142=1, reg178=48
`battery_w = None` (SELF_SUSTAIN Deye řídí sám) ⇒ pro detekci režimu se bere jako 0 ⇒ při `grid_setpoint_w = 0` je výsledek **PASSIVE**; registry 108/109 se nastaví na **plné limity z DB** (ne na nulu). `battery_w = None` a `self_sustain_local_use=True` (**SELF_SUSTAIN**) ⇒ pro `get_deye_mode` se bere jako 0 ⇒ při `grid_setpoint_w = 0` je **PASSIVE**; v `write_inverter_setpoints` se ale zapíše **108 = max** a **109 = max**, reg **142** = zero export dle DB, TOU SOC = **`min_soc_percent`** (ne ekonomická větev 100 % z ceny).
## EMS politiky (nejsou fyzické stavy Deye) ## EMS politiky (nejsou fyzické stavy Deye)