fix(control): reg 108 v PV_SURPLUS sleduje charge intent (BA81 nenabíjelo levné ráno)
deye_battery_charge_discharge_amps: v PASSIVE+PV_SURPLUS reg 108 = max když plán chce nabíjet (bat_w>0) místo tvrdé 0; baterka nabere co zvládne, přebytek nad nabíjecí rychlost do sítě. + kalibrace: SoC u maxima → dojet na 100% (BMS). Sell beze změny. Vědomě přepsán test starého chování. 365 passed. Všechny Deye lokality. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -110,7 +110,7 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st
|
|||||||
|
|
||||||
17. **Modbus zápis = journal.** Každý zápis do zařízení přes control exporter se loguje do `ems.modbus_command`. **Verifikační job** běží každé **2 minuty** a ověřuje nedávno zápis (`written` → čtení registru). Při **mismatch** po max. **3** pokusech o zápis → u běžných registrů přepnutí na **SELF_SUSTAIN** (`run_fn_set_mode_with_discord` → `fn_set_mode`, `activated_by` = `system:mismatch`) + **Discord** při skutečné změně režimu. **Výjimka:** souvislý blok Deye **62–64** (čas) → po 3 neúspěšných ověřeních **bez** změny režimu, kritický **Discord** (`notify_modbus_clock_verify_exhausted`). **Obecně:** při jakékoli změně `mode_code` z Pythonu (`POST /api/v1/sites/{id}/mode`, mismatch → SELF_SUSTAIN, `fn_expire_modes`) lze Discord zapnout přes `DISCORD_WEBHOOK_URL`. Detail: `docs/04-modules/modbus-command-journal.md`.
|
17. **Modbus zápis = journal.** Každý zápis do zařízení přes control exporter se loguje do `ems.modbus_command`. **Verifikační job** běží každé **2 minuty** a ověřuje nedávno zápis (`written` → čtení registru). Při **mismatch** po max. **3** pokusech o zápis → u běžných registrů přepnutí na **SELF_SUSTAIN** (`run_fn_set_mode_with_discord` → `fn_set_mode`, `activated_by` = `system:mismatch`) + **Discord** při skutečné změně režimu. **Výjimka:** souvislý blok Deye **62–64** (čas) → po 3 neúspěšných ověřeních **bez** změny režimu, kritický **Discord** (`notify_modbus_clock_verify_exhausted`). **Obecně:** při jakékoli změně `mode_code` z Pythonu (`POST /api/v1/sites/{id}/mode`, mismatch → SELF_SUSTAIN, `fn_expire_modes`) lze Discord zapnout přes `DISCORD_WEBHOOK_URL`. Detail: `docs/04-modules/modbus-command-journal.md`.
|
||||||
|
|
||||||
18. **Deye zápis registrů 60–499:** pouze **FC 0x10** (`write_registers`), **nikdy** FC 0x06 pro tento rozsah; **`execute_modbus_commands`** slučuje souvislé adresy do jednoho FC 0x10. **Fyzický režim Deye** (`PASSIVE` / `CHARGE` / `SELL`): **výhradně** `get_deye_mode` v `exporter_monolith.py` (bez wattových prahů: **SELL** při `battery_w` < 0 a `grid_setpoint_w` < 0; **CHARGE** při obou > 0; jinak **PASSIVE**). **PASSIVE (ZERO, AUTO):** **`export_mode=PV_SURPLUS`** → **108=0**, **109=max**, **142**=`deye_zero_export_mode` (ne selling first); jinak **108/109** dle `deye_battery_charge_discharge_amps` / `_deye_zero_export_amps_for_passive` (import bez vybíjení → **109=0**); **TOU SOC** (reg 166+): **PASSIVE** = **`min_soc_percent`**, **CHARGE** = **`max_soc_percent`** (clamp 10–100 z DB), **SELL** = **`reserve_soc_percent`** (`_deye_passive_tou_battery_soc_pct`, `_deye_tou_params`). **SELL:** reg **108** EMS **nezapisuje** (selling first = **142**), **109**=max, **178**=32 (peak shaving off), **143** omezeno podle `|grid_setpoint_w|`; **142/145/TOU** jako v `write_inverter_setpoints`. **Reg 340** (*max solar power*, W): jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (strop z `deye_reg340_max_solar_w`, typ. 32k home-01 / 65k jinde, ne součet Wp; min `deye_reg340_min_solar_w`, home-01 400); hodnota z plánu / curtailu (AUTO). **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do SELF_SUSTAIN. **PRESERVE:** `lock_battery` → 108/109=0. **Čas 62–64**, bloky TOU **1–2** vs **3–6**, verify, Discord: beze změny oproti dřívějšímu chování — plný popis **`docs/04-modules/modbus-registers.md`** a **`docs/04-modules/operating-modes.md`**.
|
18. **Deye zápis registrů 60–499:** pouze **FC 0x10** (`write_registers`), **nikdy** FC 0x06 pro tento rozsah; **`execute_modbus_commands`** slučuje souvislé adresy do jednoho FC 0x10. **Fyzický režim Deye** (`PASSIVE` / `CHARGE` / `SELL`): **výhradně** `get_deye_mode` v `exporter_monolith.py` (bez wattových prahů: **SELL** při `battery_w` < 0 a `grid_setpoint_w` < 0; **CHARGE** při obou > 0; jinak **PASSIVE**). **PASSIVE (ZERO, AUTO):** **`export_mode=PV_SURPLUS`** → reg **108 sleduje charge intent plánu** (fix 2026-06-16): `bat_w>0` → **108=max** (baterka nabere kolik fyzicky zvládne, přebytek **nad nabíjecí rychlost** do sítě — případ „výroba > rychlost baterky", BA81); SoC u maxima (`>= max_soc − BATTERY_CALIB_TOPOFF_MARGIN_PCT`) + přebytek → **108=max** (BMS kalibrace na 100 %); jen `bat_w<=0` daleko od maxima → **108=0** (prodej PV, drž baterku). **109=max**, **142**=`deye_zero_export_mode` (ne selling first); jinak **108/109** dle `deye_battery_charge_discharge_amps` / `_deye_zero_export_amps_for_passive` (import bez vybíjení → **109=0**); **TOU SOC** (reg 166+): **PASSIVE** = **`min_soc_percent`**, **CHARGE** = **`max_soc_percent`** (clamp 10–100 z DB), **SELL** = **`reserve_soc_percent`** (`_deye_passive_tou_battery_soc_pct`, `_deye_tou_params`). **SELL:** reg **108** EMS **nezapisuje** (selling first = **142**), **109**=max, **178**=32 (peak shaving off), **143** omezeno podle `|grid_setpoint_w|`; **142/145/TOU** jako v `write_inverter_setpoints`. **Reg 340** (*max solar power*, W): jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (strop z `deye_reg340_max_solar_w`, typ. 32k home-01 / 65k jinde, ne součet Wp; min `deye_reg340_min_solar_w`, home-01 400); hodnota z plánu / curtailu (AUTO). **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do SELF_SUSTAIN. **PRESERVE:** `lock_battery` → 108/109=0. **Čas 62–64**, bloky TOU **1–2** vs **3–6**, verify, Discord: beze změny oproti dřívějšímu chování — plný popis **`docs/04-modules/modbus-registers.md`** a **`docs/04-modules/operating-modes.md`**.
|
||||||
|
|
||||||
19. **HARD LIMIT exportu na fakturačním elektroměru — NIKDY nepřekročit.** Překročení rezervovaného exportního výkonu (home-01: 13.5 kW) byť o desetiny kW = smluvní pokuta v řádu desítek tisíc Kč za kW. Jediný bezpečný invariant: **reg 143 (limit na svorkách střídače) <= max_export_power_w (limit ulice) VŽDY** — v nejhorším případě (spotřeba mezi střídačem a CT odpadne) je ulice rovna svorkám. **ZAKÁZÁNO** jakékoli feed-forward navyšování terminálového limitu o měřenou spotřebu (výpadek spotřeby = přestřelení ulice). Vyšší vytěžení smí přinést jedině interní regulace střídače proti CT (firmware smyčka), nikdy náš software s 1min telemetrií a 15min ticky.
|
19. **HARD LIMIT exportu na fakturačním elektroměru — NIKDY nepřekročit.** Překročení rezervovaného exportního výkonu (home-01: 13.5 kW) byť o desetiny kW = smluvní pokuta v řádu desítek tisíc Kč za kW. Jediný bezpečný invariant: **reg 143 (limit na svorkách střídače) <= max_export_power_w (limit ulice) VŽDY** — v nejhorším případě (spotřeba mezi střídačem a CT odpadne) je ulice rovna svorkám. **ZAKÁZÁNO** jakékoli feed-forward navyšování terminálového limitu o měřenou spotřebu (výpadek spotřeby = přestřelení ulice). Vyšší vytěžení smí přinést jedině interní regulace střídače proti CT (firmware smyčka), nikdy náš software s 1min telemetrií a 15min ticky.
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ async def write_inverter_setpoints(
|
|||||||
max_discharge_a=int(inv.max_discharge_a),
|
max_discharge_a=int(inv.max_discharge_a),
|
||||||
export_mode=setpoints_now.export_mode,
|
export_mode=setpoints_now.export_mode,
|
||||||
export_ban=bool(setpoints_now.export_ban),
|
export_ban=bool(setpoints_now.export_ban),
|
||||||
|
current_soc_pct=soc_telemetry,
|
||||||
|
max_soc_pct=inv.max_soc_percent,
|
||||||
)
|
)
|
||||||
|
|
||||||
zero_exp_mode = int(inv.deye_zero_export_mode or 1)
|
zero_exp_mode = int(inv.deye_zero_export_mode or 1)
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ from services.control.models import ControlSetpoints, InverterConfig, OperatingM
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
#: Tolerance pod max SoC, v rámci níž se v PV přebytku nechá baterka dojet na max
|
||||||
|
#: (reg 108 = max) kvůli BMS rekalibraci SoC (LiFePO4 potřebuje občas na 100 %).
|
||||||
|
BATTERY_CALIB_TOPOFF_MARGIN_PCT = 3.0
|
||||||
|
|
||||||
|
|
||||||
def _deye_system_time_register_rows() -> tuple[datetime, list[tuple[int, str, int]]]:
|
def _deye_system_time_register_rows() -> tuple[datetime, list[tuple[int, str, int]]]:
|
||||||
"""Hodnoty pro reg 62-64 (Europe/Prague); sekundy v reg 64 = 0 (stabilnější zápis)."""
|
"""Hodnoty pro reg 62-64 (Europe/Prague); sekundy v reg 64 = 0 (stabilnější zápis)."""
|
||||||
@@ -437,12 +441,20 @@ def deye_battery_charge_discharge_amps(
|
|||||||
max_discharge_a: int,
|
max_discharge_a: int,
|
||||||
export_mode: str | None = None,
|
export_mode: str | None = None,
|
||||||
export_ban: bool = False,
|
export_ban: bool = False,
|
||||||
|
current_soc_pct: float | None = None,
|
||||||
|
max_soc_pct: int | None = None,
|
||||||
) -> tuple[int | None, int]:
|
) -> tuple[int | None, int]:
|
||||||
"""
|
"""
|
||||||
Proud nabíjení / vybíjení (reg 108 / 109) pro zápis Deye.
|
Proud nabíjení / vybíjení (reg 108 / 109) pro zápis Deye.
|
||||||
|
|
||||||
**PV_SURPLUS** (PASSIVE, export FVE): **108 = 0**, **109 = max** — baterie se přes limit
|
**PV_SURPLUS** (PASSIVE, export FVE) — reg 108 SLEDUJE charge intent plánu (fix 2026-06-16):
|
||||||
nabíjení neplní, přebytek jde do sítě (142 = zero-export dle instalace, 145 = 1).
|
- `bat_w > 0` (plán chce nabíjet z přebytku) → **108 = max**: baterie nabere kolik fyzicky
|
||||||
|
zvládne (nabíjecí rychlost), přebytek NAD ni jde do sítě (BA81: výroba 12 kW > rychlost
|
||||||
|
6 kW → 6 do baterky, 6 ven). Dřív tvrdě 108=0 i při bat_w>0 → baterka nenabíjela ani
|
||||||
|
levné ranní PV (control bug).
|
||||||
|
- kalibrace: SoC u maxima (`>= max_soc − margin`) + přebytek → **108 = max**, ať dojede na
|
||||||
|
100 % (BMS rekalibrace SoC). Strop drží Deye max_soc.
|
||||||
|
- jen „prodej PV a drž baterku" daleko od maxima (`bat_w <= 0`) → **108 = 0**, přebytek ven.
|
||||||
|
|
||||||
PASSIVE + nabíjení bez exportního záměru (`battery_w > 0`, export_mode NONE): **108 = max**.
|
PASSIVE + nabíjení bez exportního záměru (`battery_w > 0`, export_mode NONE): **108 = max**.
|
||||||
**CHARGE** ze sítě: 108 z `battery_w`.
|
**CHARGE** ze sítě: 108 z `battery_w`.
|
||||||
@@ -464,6 +476,16 @@ def deye_battery_charge_discharge_amps(
|
|||||||
export_ban=export_ban,
|
export_ban=export_ban,
|
||||||
grid_w=grid_w,
|
grid_w=grid_w,
|
||||||
):
|
):
|
||||||
|
# reg 108 sleduje charge intent: nabíjet z přebytku (bat_w>0) nebo dojet na max
|
||||||
|
# kvůli BMS kalibraci (SoC u maxima + přebytek) → 108 = max; jinak 108 = 0 (přebytek
|
||||||
|
# ven). Strop SoC drží Deye max_soc, takže 108=max nepřebije nad povolené.
|
||||||
|
near_full_calib = (
|
||||||
|
current_soc_pct is not None
|
||||||
|
and max_soc_pct is not None
|
||||||
|
and float(current_soc_pct) >= float(max_soc_pct) - BATTERY_CALIB_TOPOFF_MARGIN_PCT
|
||||||
|
)
|
||||||
|
if bat_w > 0 or near_full_calib:
|
||||||
|
return int(max_charge_a), int(max_discharge_a)
|
||||||
return 0, int(max_discharge_a)
|
return 0, int(max_discharge_a)
|
||||||
if bat_w > 0:
|
if bat_w > 0:
|
||||||
return int(max_charge_a), int(max_discharge_a)
|
return int(max_charge_a), int(max_discharge_a)
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
"""PASSIVE + PV_SURPLUS: 108=0 (nepoužívat baterii), 109=max; 142 zůstává zero-export (1/2)."""
|
"""PASSIVE + PV_SURPLUS: reg 108 sleduje charge intent (fix 2026-06-16).
|
||||||
|
|
||||||
|
bat_w>0 (plán chce nabíjet z přebytku) → 108=max (baterka nabere co zvládne, zbytek ven);
|
||||||
|
SoC u maxima + přebytek → 108=max (BMS kalibrace na 100 %); jen "prodej PV a drž baterku"
|
||||||
|
daleko od maxima (bat_w<=0) → 108=0. 109=max, 142 zůstává zero-export (1/2).
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -23,8 +28,11 @@ class PassivePvSurplusChargeAmpsTests(unittest.TestCase):
|
|||||||
self.assertEqual(ch, 0)
|
self.assertEqual(ch, 0)
|
||||||
self.assertEqual(dis, 90)
|
self.assertEqual(dis, 90)
|
||||||
|
|
||||||
def test_pv_surplus_even_if_lp_shows_positive_battery_w(self) -> None:
|
def test_pv_surplus_with_positive_battery_w_charges_at_max(self) -> None:
|
||||||
"""Plán může mít kladný battery_w; exportní záměr je PV_SURPLUS → 108=0."""
|
"""Fix 2026-06-16: plán chce nabíjet z přebytku (bat_w>0) → 108=max (ne 0).
|
||||||
|
|
||||||
|
Baterka nabere kolik zvládne, přebytek nad nabíjecí rychlost jde do sítě (BA81).
|
||||||
|
"""
|
||||||
ch, dis = deye_battery_charge_discharge_amps(
|
ch, dis = deye_battery_charge_discharge_amps(
|
||||||
lock_battery=False,
|
lock_battery=False,
|
||||||
deye_mode="PASSIVE",
|
deye_mode="PASSIVE",
|
||||||
@@ -36,6 +44,41 @@ class PassivePvSurplusChargeAmpsTests(unittest.TestCase):
|
|||||||
export_mode="PV_SURPLUS",
|
export_mode="PV_SURPLUS",
|
||||||
export_ban=False,
|
export_ban=False,
|
||||||
)
|
)
|
||||||
|
self.assertEqual(ch, 100)
|
||||||
|
self.assertEqual(dis, 100)
|
||||||
|
|
||||||
|
def test_pv_surplus_near_full_tops_off_for_calibration(self) -> None:
|
||||||
|
"""SoC u maxima (97 >= 100-3) + přebytek → 108=max i při bat_w<=0 (BMS kalibrace)."""
|
||||||
|
ch, dis = deye_battery_charge_discharge_amps(
|
||||||
|
lock_battery=False,
|
||||||
|
deye_mode="PASSIVE",
|
||||||
|
self_sustain_local_use=False,
|
||||||
|
bat_w=0,
|
||||||
|
grid_w=-2000,
|
||||||
|
max_charge_a=100,
|
||||||
|
max_discharge_a=100,
|
||||||
|
export_mode="PV_SURPLUS",
|
||||||
|
export_ban=False,
|
||||||
|
current_soc_pct=97.0,
|
||||||
|
max_soc_pct=100,
|
||||||
|
)
|
||||||
|
self.assertEqual(ch, 100)
|
||||||
|
|
||||||
|
def test_pv_surplus_sell_hold_far_from_full_zeros_charge(self) -> None:
|
||||||
|
"""Prodej PV a drž baterku daleko od maxima (bat_w<=0, SoC nízko) → 108=0."""
|
||||||
|
ch, dis = deye_battery_charge_discharge_amps(
|
||||||
|
lock_battery=False,
|
||||||
|
deye_mode="PASSIVE",
|
||||||
|
self_sustain_local_use=False,
|
||||||
|
bat_w=0,
|
||||||
|
grid_w=-2000,
|
||||||
|
max_charge_a=100,
|
||||||
|
max_discharge_a=100,
|
||||||
|
export_mode="PV_SURPLUS",
|
||||||
|
export_ban=False,
|
||||||
|
current_soc_pct=60.0,
|
||||||
|
max_soc_pct=100,
|
||||||
|
)
|
||||||
self.assertEqual(ch, 0)
|
self.assertEqual(ch, 0)
|
||||||
self.assertEqual(dis, 100)
|
self.assertEqual(dis, 100)
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Vychází z **`grid_setpoint_w`** a **`battery_w`** z `ControlSetpoints` (aktivn
|
|||||||
|
|
||||||
Režim **CHARGE_CHEAP** nastaví oba setpointy na stejný kladný výkon (min. 1 W), aby byl výsledek **CHARGE**.
|
Režim **CHARGE_CHEAP** nastaví oba setpointy na stejný kladný výkon (min. 1 W), aby byl výsledek **CHARGE**.
|
||||||
|
|
||||||
**PASSIVE (ZERO):** u slotu **`export_mode = PV_SURPLUS`** exporter nastaví **108 = 0** (nabíjecí proud), **109 = max** — baterie nemá kam brát přebytek FVE, jde do sítě při **145 = 1**; **142** zůstává **`deye_zero_export_mode`** (u CT často **2** = zero export k měření zátěže, ne selling first z baterie). Detail: `operating-modes.md`.
|
**PASSIVE (ZERO):** u slotu **`export_mode = PV_SURPLUS`** reg **108** (nabíjecí proud) **sleduje charge intent plánu** (fix 2026-06-16): `bat_w > 0` → **108 = max** (baterka nabere kolik fyzicky zvládne, přebytek **nad nabíjecí rychlost** jde do sítě při **145 = 1** — řeší případ „výroba > rychlost baterky" na export-omezených i běžných lokalitách); SoC u maxima (`>= max_soc − 3 p.b.`, `BATTERY_CALIB_TOPOFF_MARGIN_PCT`) + přebytek → **108 = max** (BMS rekalibrace na 100 %); jen „prodej PV a drž baterku" daleko od maxima (`bat_w <= 0`) → **108 = 0**. **109 = max**; **142** zůstává **`deye_zero_export_mode`** (u CT často **2**). Dřív tvrdě **108 = 0** i při `bat_w > 0` → baterka nenabíjela ani levné ranní PV (control bug, BA81). Detail: `operating-modes.md`, changelog 2026-06-16.
|
||||||
|
|
||||||
### BA81: GEN port cut-off (reg 178 bits0–1) z plánu
|
### BA81: GEN port cut-off (reg 178 bits0–1) z plánu
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- **Žá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`).
|
- **Žá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.
|
- **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)** = **142** = `deye_zero_export_mode` (1/2, ne selling first). **PV_SURPLUS:** **108 = 0**, **109 = max** — přebytek FVE do sítě (**145 = 1**), ne do baterie. Jinak **108/109** typicky max; výjimka import bez vybíjení → **109 = 0**.
|
- **ZERO (PASSIVE)** = **142** = `deye_zero_export_mode` (1/2, ne selling first). **PV_SURPLUS** (fix 2026-06-16): reg **108 sleduje charge intent plánu** — `bat_w > 0` → **108 = max** (baterka nabere kolik fyzicky zvládne, přebytek **nad nabíjecí rychlost** jde do sítě, **145 = 1**); SoC u maxima (`>= max_soc − 3 p.b.`) + přebytek → **108 = max** (BMS kalibrace na 100 %); jen „prodej PV a drž baterku" daleko od maxima (`bat_w <= 0`) → **108 = 0**. **109 = max**. Jinak **108/109** typicky max; výjimka import bez vybíjení → **109 = 0**.
|
||||||
- **SELL** = plánovaný export **i** plánované vybíjení (oba záporné) → **142** = selling first, **178** = vypnutý grid peak shaving (32); reg **108** EMS **nemění** (export řídí 142, ne vynucené 0 A). Po návratu do ZERO/CHARGE zase **178** = 48.
|
- **SELL** = plánovaný export **i** plánované vybíjení (oba záporné) → **142** = selling first, **178** = vypnutý grid peak shaving (32); reg **108** EMS **nemění** (export řídí 142, ne vynucené 0 A). 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`).
|
- Novou logiku vždy ověřit proti **reálnému řádku plánu** (audit / `planning_interval`).
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-06-16 — control: reg 108 v PV_SURPLUS sleduje charge intent (BA81 nenabíjelo levné ráno)
|
||||||
|
|
||||||
|
- **Problém (triáž BA81):** výroba 12 kW (= ~2× nabíjecí rychlost baterky 6 kW), levné ranní výkupní ceny, baterka stála celé ráno na 29 % a vše šlo do sítě; nabíjet začala až odpoledne (dražší). Plán PŘITOM chtěl nabíjet (soc_tgt rostl), ale realita ne → promeškaná levná ranní arbitráž (~0.7 Kč/kWh). NEbyl to forecast (canonical ≈ realita) ani planner — **exekuce.**
|
||||||
|
- **Příčina:** `deye_battery_charge_discharge_amps` (setpoints.py) v PASSIVE + `export_mode=PV_SURPLUS` vracela tvrdě **`108=0` i když `bat_w>0`** (záměrné, testem podchycené chování — ale chyba pro „výroba > nabíjecí rychlost"). Deye pak prodával vše, baterku nenabil. `get_deye_mode`: `bat_w>0 & grid<0` (export) → PASSIVE, ne CHARGE.
|
||||||
|
- **Mechanismus (fix):** reg 108 v PV_SURPLUS **sleduje charge intent plánu**: `bat_w>0` → **108=max** (baterka nabere kolik fyzicky zvládne, přebytek nad rychlost do sítě); SoC u maxima (`>= max_soc − 3 p.b.`) + přebytek → **108=max** (BMS rekalibrace na 100 %); jen `bat_w<=0` daleko od maxima → **108=0**. Sell/discharge beze změny (mód + 109, 108 neřešíme — díky DV za korekci). Strop SoC drží Deye max_soc.
|
||||||
|
- **Soubory:** `setpoints.py` (`deye_battery_charge_discharge_amps` + konstanta `BATTERY_CALIB_TOPOFF_MARGIN_PCT`), `inverter.py` (napojení živého SoC + max_soc), `test_control_deye_passive_pv_charge.py` (vědomě přepsán test starého chování + 2 nové), CLAUDE.md §18, operating-modes.md, modbus-registers.md.
|
||||||
|
- **Ověření:** plná sada **365 passed, 4 xfailed**. Mimo solver → golden gate beze změny. Platí pro všechny Deye lokality (BA81 i hypotetická malá s nízkým export limitem).
|
||||||
|
|
||||||
## 2026-06-14 — EV anti-fragmentace + 3f power floor (Fix B, solver_v2)
|
## 2026-06-14 — EV anti-fragmentace + 3f power floor (Fix B, solver_v2)
|
||||||
|
|
||||||
- **Problém:** EV nabíjení v solveru spojité po slotech bez start/stop penalty → rozsekané přes nesouvislé sloty + dílčí 1f trickle (sub-6A, který control stejně shazoval na 0 A) → cyklování nabíječky, Tesla notifikace.
|
- **Problém:** EV nabíjení v solveru spojité po slotech bez start/stop penalty → rozsekané přes nesouvislé sloty + dílčí 1f trickle (sub-6A, který control stejně shazoval na 0 A) → cyklování nabíječky, Tesla notifikace.
|
||||||
|
|||||||
Reference in New Issue
Block a user