HARD LIMIT exportu jako tvrdé pravidlo §4.19 + test
Překročení rezervovaného exportu na fakturačním elektroměru (home-01 13.5 kW) = pokuta v řádu desítek tisíc Kč/kW. Invariant: reg 143 (svorky) <= max_export_power_w (ulice) VŽDY; feed-forward navyšování o měřenou spotřebu mezi střídačem a CT ZAKÁZÁNO (výpadek spotřeby = přestřelení ulice). Návrh feed-forwardu z 2026-06-12 večer zavržen před implementací na pokyn uživatele. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -112,7 +112,9 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st
|
||||
|
||||
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`**.
|
||||
|
||||
19. **Baterie – export v LP:** V `solve_dispatch` binárka `z_export[t]`: pokud `grid_export` v daném slotu **≥ 1** W, platí koncové `soc[t] ≥ arb_base_wh` (ekonomická rezerva z DB, ne časová řada `arb_floor_series`). Bez exportu může plán jít k `min_soc_percent` (provozní podlaha; u paralelních packů často 11–12 %, migrace V029 + komentář sloupce).
|
||||
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.
|
||||
|
||||
20. **Baterie – export v LP:** V `solve_dispatch` binárka `z_export[t]`: pokud `grid_export` v daném slotu **≥ 1** W, platí koncové `soc[t] ≥ arb_base_wh` (ekonomická rezerva z DB, ne časová řada `arb_floor_series`). Bez exportu může plán jít k `min_soc_percent` (provozní podlaha; u paralelních packů často 11–12 %, migrace V029 + komentář sloupce).
|
||||
|
||||
---
|
||||
|
||||
|
||||
35
backend/tests/test_export_hard_limit.py
Normal file
35
backend/tests/test_export_hard_limit.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""HARD LIMIT exportu (CLAUDE.md §4.19): reg 143 nikdy nad limit ulice.
|
||||
|
||||
Pokuta v řádu desítek tisíc Kč za každou kW překročení rezervovaného
|
||||
exportního výkonu na fakturačním elektroměru. Terminálový limit (reg 143)
|
||||
nesmí přesáhnout max_export_power_w za žádných okolností — žádný
|
||||
feed-forward o měřenou spotřebu mezi střídačem a CT.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from services.control.setpoints import _deye_reg143_export_w
|
||||
|
||||
|
||||
class ExportHardLimitTests(unittest.TestCase):
|
||||
def test_reg143_never_exceeds_street_limit(self) -> None:
|
||||
street_limit = 13_500
|
||||
self.assertLessEqual(
|
||||
_deye_reg143_export_w(False, street_limit), street_limit
|
||||
)
|
||||
|
||||
def test_no_export_is_zero(self) -> None:
|
||||
self.assertEqual(_deye_reg143_export_w(True, 13_500), 0)
|
||||
|
||||
def test_plan_export_limit_caps_not_raises(self) -> None:
|
||||
# vzor z write_inverter_setpoints: export_lim = min(hw, plan) — plán
|
||||
# smí limit jen SNÍŽIT, nikdy zvýšit
|
||||
hw = _deye_reg143_export_w(False, 13_500)
|
||||
plan_limit = 20_000
|
||||
self.assertLessEqual(min(hw, plan_limit), 13_500)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user