diff --git a/CLAUDE.md b/CLAUDE.md index 0ec99f2..b3e483d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -108,7 +108,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`. -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:** 108=0, 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)` (lokalita se zeleným bonusem na PV poli) **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0`; hodnota z `pv_a_forecast_solver_w` / `pv_a_curtailed_w` (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`** → **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:** 108=0, 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). diff --git a/backend/services/control/deye_helpers.py b/backend/services/control/deye_helpers.py index cea7ba7..b3078f1 100644 --- a/backend/services/control/deye_helpers.py +++ b/backend/services/control/deye_helpers.py @@ -65,7 +65,7 @@ DEYE_REGISTER_NAMES: dict[int, str] = { 142: "limit_control (0=selling first, 1=zero export to load, 2=zero export to CT)", 143: "export_limit_w (max export do sítě)", 145: "solar_sell (0=disabled, 1=enabled)", - 340: "max_solar_power_w (strop DC PV A v W; součet nominal_power_wp řiditelných polí)", + 340: "max_solar_power_w (strop DC PV A v W; cap z fn_inverter_pv_a_max_w / deye_reg340_max_solar_w)", 178: "control_board_special_1 (bits0-1: MI export cutoff disable=2 enable=3; bits4-5 peak shaving 32/48)", 148: "time_point_1_time", 149: "time_point_2_time", @@ -157,11 +157,21 @@ def next_slot_hhmm() -> int: return next_hour * 100 + next_min -def compute_pv_a_reg340_max_solar_w(cap_w: int, forecast_w: int, curtail_w: int) -> int: +def compute_pv_a_reg340_max_solar_w( + cap_w: int, + forecast_w: int, + curtail_w: int, + *, + min_w: int = 0, +) -> int: """Hodnota pro Deye reg 340 (max solar power, W) z capu a plánovaného curtailmentu pole A.""" if curtail_w <= 0: - return int(cap_w) - return max(0, min(int(cap_w), int(forecast_w) - int(curtail_w))) + raw = int(cap_w) + else: + raw = max(0, min(int(cap_w), int(forecast_w) - int(curtail_w))) + if raw > 0 and int(min_w) > 0: + return max(int(min_w), raw) + return raw def _prague_minute_start_utc() -> datetime: diff --git a/backend/services/control/models.py b/backend/services/control/models.py index 35308a0..9a65676 100644 --- a/backend/services/control/models.py +++ b/backend/services/control/models.py @@ -30,8 +30,10 @@ class InverterConfig: deye_tou_inactive_signature: str | None = None deye_zero_export_mode: int = 1 deye_gen_microinverter_cutoff_enabled: bool = False - #: Součet nominal_power_wp controllable PV na invertoru; 0 = EMS nezapisuje reg 340. + #: Strop reg 340 (W) z fn_inverter_pv_a_max_w; 0 = EMS nezapisuje reg 340. pv_a_cap_w: int = 0 + #: Minimální hodnota reg 340 přijatá firmwarem (0 nebo např. 400 u staršího Deye). + pv_a_reg340_min_w: int = 0 #: True = EMS smí řídit Deye reg 340 (max solar power); z SQL `fn_site_has_active_green_bonus_pv(site_id)`. deye_reg340_pv_a_control_enabled: bool = False diff --git a/backend/services/control/orchestrator.py b/backend/services/control/orchestrator.py index 1ddbd7f..7cd7eec 100644 --- a/backend/services/control/orchestrator.py +++ b/backend/services/control/orchestrator.py @@ -38,6 +38,7 @@ async def export_setpoints(site_id: int, db: asyncpg.Connection) -> None: try: inv_for_pv = await _load_inverter_config(site_id, db) cap_pv = int(inv_for_pv.pv_a_cap_w) if inv_for_pv is not None else 0 + min_pv = int(inv_for_pv.pv_a_reg340_min_w) if inv_for_pv is not None else 0 reg340_en = ( bool(inv_for_pv.deye_reg340_pv_a_control_enabled) if inv_for_pv is not None @@ -49,12 +50,14 @@ async def export_setpoints(site_id: int, db: asyncpg.Connection) -> None: mode, pi_now, pv_a_cap_w=cap_pv, + pv_a_reg340_min_w=min_pv, reg340_pv_a_control_enabled=reg340_en, ) sp_next = _build_setpoints( mode, pi_next, pv_a_cap_w=cap_pv, + pv_a_reg340_min_w=min_pv, reg340_pv_a_control_enabled=reg340_en, ) diff --git a/backend/services/control/repository.py b/backend/services/control/repository.py index 087404a..6e2b913 100644 --- a/backend/services/control/repository.py +++ b/backend/services/control/repository.py @@ -78,6 +78,7 @@ async def _load_inverter_config( SELECT ai.id, ai.code, coalesce(ems.fn_inverter_pv_a_max_w(ai.id), 0) AS pv_a_cap_w, + coalesce(ai.deye_reg340_min_solar_w, 0) AS pv_a_reg340_min_w, se.host, se.port, se.unit_id, sgc.max_export_power_w, sgc.max_import_power_w, @@ -182,6 +183,7 @@ async def _load_inverter_config( row["deye_gen_microinverter_cutoff_enabled"] or False ), pv_a_cap_w=int(row["pv_a_cap_w"] or 0), + pv_a_reg340_min_w=int(row["pv_a_reg340_min_w"] or 0), deye_reg340_pv_a_control_enabled=bool( row["deye_reg340_pv_a_control_enabled"] or False ), diff --git a/backend/services/control/setpoints.py b/backend/services/control/setpoints.py index efe340f..22d3c5e 100644 --- a/backend/services/control/setpoints.py +++ b/backend/services/control/setpoints.py @@ -97,6 +97,7 @@ def _build_setpoints( pi: Any | None, *, pv_a_cap_w: int = 0, + pv_a_reg340_min_w: int = 0, reg340_pv_a_control_enabled: bool = False, ) -> ControlSetpoints | None: code = mode.mode_code @@ -154,7 +155,10 @@ def _build_setpoints( pv_a_allowed = None else: pv_a_allowed = compute_pv_a_reg340_max_solar_w( - int(pv_a_cap_w), forecast, curtail + int(pv_a_cap_w), + forecast, + curtail, + min_w=int(pv_a_reg340_min_w), ) return ControlSetpoints( battery_w=bat_w, diff --git a/backend/tests/test_control_exporter_reg340.py b/backend/tests/test_control_exporter_reg340.py index 1e30954..57d7d07 100644 --- a/backend/tests/test_control_exporter_reg340.py +++ b/backend/tests/test_control_exporter_reg340.py @@ -52,6 +52,15 @@ class ComputePvAReg340Tests(unittest.TestCase): def test_curtail_floor_zero(self) -> None: self.assertEqual(compute_pv_a_reg340_max_solar_w(10_000, 1000, 5000), 0) + def test_min_clamp_when_positive(self) -> None: + self.assertEqual( + compute_pv_a_reg340_max_solar_w(32_000, 5000, 4600, min_w=400), + 400, + ) + + def test_min_not_applied_when_curtail_to_zero(self) -> None: + self.assertEqual(compute_pv_a_reg340_max_solar_w(10_000, 1000, 5000, min_w=400), 0) + class BuildSetpointsReg340Tests(unittest.TestCase): def test_with_cap_sets_pv_a_allowed(self) -> None: diff --git a/db/migration/V082__deye_reg340_inverter_limits.sql b/db/migration/V082__deye_reg340_inverter_limits.sql new file mode 100644 index 0000000..1d1c018 --- /dev/null +++ b/db/migration/V082__deye_reg340_inverter_limits.sql @@ -0,0 +1,33 @@ +-- Reg 340 (max solar power): strop dle výkonu střídače, ne součtu Wp polí; min dle firmware. +alter table ems.asset_inverter + add column if not exists deye_reg340_max_solar_w int, + add column if not exists deye_reg340_min_solar_w int not null default 0; + +comment on column ems.asset_inverter.deye_reg340_max_solar_w is + 'Horní strop zápisu Deye reg 340 (max solar power, W). Studené panely mohou překročit součet Wp — použít plný DC limit střídače (např. 32000 home-01, 65000 větší hybridy). NULL = fallback max_dc_input_w, pak součet Wp řiditelných polí.'; + +comment on column ems.asset_inverter.deye_reg340_min_solar_w is + 'Minimální hodnota reg 340 přijatá firmwarem střídače (W). 0 = bez spodního limitu; starší Deye (home-01) často 400.'; + +-- home-01: SUN-20K, reg 340 max 32 kW, firmware min 400 W +update ems.asset_inverter inv +set + deye_reg340_max_solar_w = 32000, + deye_reg340_min_solar_w = 400 +from ems.site s +where s.id = inv.site_id + and s.code = 'home-01' + and inv.code = 'deye-main' + and inv.controllable = true; + +-- Ostatní řízené Deye hybridy: 65 kW strop, min 0 (novější firmware) +update ems.asset_inverter inv +set + deye_reg340_max_solar_w = coalesce(inv.deye_reg340_max_solar_w, 65000), + deye_reg340_min_solar_w = 0 +from ems.site s +where s.id = inv.site_id + and s.code <> 'home-01' + and inv.code = 'deye-main' + and inv.controllable = true + and inv.active = true; diff --git a/db/routines/R__083_fn_inverter_pv_a_max_w.sql b/db/routines/R__083_fn_inverter_pv_a_max_w.sql index ca44821..3687b13 100644 --- a/db/routines/R__083_fn_inverter_pv_a_max_w.sql +++ b/db/routines/R__083_fn_inverter_pv_a_max_w.sql @@ -1,14 +1,27 @@ --- Cap pro reg 340 max solar power (W): součet nominal_power_wp řiditelných PV polí na invertoru. +-- Cap pro reg 340 max solar power (W): plný výkon střídače, ne jen součet Wp polí A. create or replace function ems.fn_inverter_pv_a_max_w(p_inverter_id int) returns int language sql stable as $$ - select coalesce(sum(nominal_power_wp), 0)::int - from ems.asset_pv_array - where inverter_id = p_inverter_id - and controllable = true + with pv as ( + select coalesce(sum(nominal_power_wp), 0)::int as wp_sum + from ems.asset_pv_array + where inverter_id = p_inverter_id + and controllable = true + ) + select case + when (select wp_sum from pv) <= 0 then 0 + else coalesce( + nullif(ai.deye_reg340_max_solar_w, 0), + nullif(ai.max_dc_input_w, 0), + (select wp_sum from pv), + 0 + )::int + end + from ems.asset_inverter ai + where ai.id = p_inverter_id $$; comment on function ems.fn_inverter_pv_a_max_w(int) is - 'Cap pro reg 340 (max solar power, W) = součet nominal_power_wp řiditelných PV polí na daném invertoru. 0 = EMS reg 340 neaktivní (skip zápisu).'; + 'Cap pro reg 340 (max solar power, W): deye_reg340_max_solar_w, jinak max_dc_input_w, jinak součet Wp řiditelných polí. 0 = bez řiditelného PV A nebo bez capu — EMS reg 340 nezapisuje.'; diff --git a/docs/03-data-model.md b/docs/03-data-model.md index 1cabb4f..31bfe33 100644 --- a/docs/03-data-model.md +++ b/docs/03-data-model.md @@ -135,7 +135,7 @@ CREATE TABLE asset_battery ( ### `asset_pv_array` Každé FVE pole zvlášť – důležité pro predikci (azimut, sklon). **Zelený bonus** (dotace za vyrobenou elektřinu) se váže na **úroveň pole**, ne na celou lokalitu: které pole má bonus, jaká je sazba Kč/kWh a platnost, určují sloupce `green_bonus_*`. Výpočet příjmu za interval probíhá funkcí `ems.fn_green_bonus_revenue` z výroby v Wh; není součástí efektivní prodejní ceny ze sítě. -**Deye reg 340 (max solar power, W):** strop pro řiditelné DC pole A na hybridu počítá `ems.fn_inverter_pv_a_max_w(inverter_id)` jako **součet `nominal_power_wp`** řádků s `controllable = true` vázaných na daný invertor. Zápis z EMS je povolen jen na lokalitách se **zeleným bonusem na PV poli** (`ems.fn_site_has_active_green_bonus_pv(site_id)` — aktivní `asset_pv_array.green_bonus_*` v kalendářním dni Europe/Prague); jinak EMS reg 340 nemění (invertor zůstane na poslední hodnotě). +**Deye reg 340 (max solar power, W):** strop `ems.fn_inverter_pv_a_max_w(inverter_id)` = `asset_inverter.deye_reg340_max_solar_w` (seed home-01 **32 000**, ostatní Deye **65 000**), fallback `max_dc_input_w`, pak součet Wp řiditelných polí; funkce vrací **0** bez řiditelného PV A. Spodní limit zápisu: `deye_reg340_min_solar_w` (home-01 **400**, jinde **0**). Zápis jen se zeleným bonusem (`fn_site_has_active_green_bonus_pv`). ```sql CREATE TABLE asset_pv_array ( diff --git a/docs/04-modules/control.md b/docs/04-modules/control.md index 2981d07..a83fc11 100644 --- a/docs/04-modules/control.md +++ b/docs/04-modules/control.md @@ -144,7 +144,7 @@ bits 0–1). Detail registrů: [`modbus-registers.md`](modbus-registers.md) (reg ### PV A curtailment — zápis reg 340 (max solar power) - **Implementace:** `backend/services/control/exporter_monolith.py` — `export_setpoints` načte cap v `_load_inverter_config` (`ems.fn_inverter_pv_a_max_w(ai.id)`), `_build_setpoints` v režimu **AUTO** dopočítá `ControlSetpoints.pv_a_allowed_w`, `write_inverter_setpoints` zařadí **reg 340**, pokud je `fn_site_has_active_green_bonus_pv` aktivní, cap > 0 a `pv_a_allowed_w` je vyplněné. -- **Data:** `pv_a_forecast_solver_w` / `pv_a_curtailed_w` z aktivního `planning_interval` (json z `ems.fn_planning_interval_at_offset`); cap = součet `nominal_power_wp` řiditelných polí na invertoru (bez nového sloupce v DB). +- **Data:** `pv_a_forecast_solver_w` / `pv_a_curtailed_w` z aktivního `planning_interval`; cap = `fn_inverter_pv_a_max_w` (`deye_reg340_max_solar_w` na `asset_inverter`, home-01 **32 kW**, ostatní **65 kW**); min = `deye_reg340_min_solar_w` (home-01 **400 W**). - **Policy PV A off (jen na site se zeleným bonusem na PV):** pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` a v aktuálním slotu zároveň `effective_buy_price < 0` a `effective_sell_price < 0` a `pv_b_forecast_solver_w > 0` (PV B vyrábí), exporter nastaví `pv_a_allowed_w = 0` (reg 340) i když je forecast PV A nulový — cílem je držet headroom v baterii pro PV B / další záporný nákup. - **Bez zápisu reg 340:** `plan_skips_deye_reg340_write` — žádný export z plánu, `battery_setpoint_w ≤ 0`, `pv_a_curtailed_w = 0` → `pv_a_allowed_w = None` (invertor řídí pole A sám). Ověření: `pytest backend/tests/test_control_exporter_reg340.py`. - **Verify:** reg **340** není kritický → po 3× mismatch verify **bez** přepnutí do SELF_SUSTAIN (stejně jako reg 178); viz [`modbus-command-journal.md`](modbus-command-journal.md). diff --git a/docs/04-modules/modbus-registers.md b/docs/04-modules/modbus-registers.md index 383721a..f935860 100644 --- a/docs/04-modules/modbus-registers.md +++ b/docs/04-modules/modbus-registers.md @@ -19,7 +19,7 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi | 141 | Energy mgmt mode | bitmask | — | EMS vždy **0** (neměnit jinak) | | 142 | Limit control (System work mode) | 0/1/2 | — | **0** = selling first, **1** = zero export to load, **2** = zero export to CT. Hodnota v non-SELL režimech pochází z `asset_inverter.deye_zero_export_mode` (závisí na instalaci – viz tabulka níže). V režimu SELL vždy **0**. | | 145 | Solar sell | 0/1 | — | **0** = disabled (přebytek FVE na **straně měniče** se nesmí vést do sítě — curtailment vůči síti), **1** = enabled. Platí jen pro **FVE pod kontrolou Deye** (`controllable = true`); druhá pole (např. **pv-b** u home-01) EMS tímto registerem neřídí. EMS dnes **vždy zapisuje 1**; směr přebytku (baterie vs. síť) řeší energie management měniče a **142**, ne umělé **108 = 0** (viz pass-through níže). | -| 340 | Max solar power | 0 … cap (W) | 1 W | Strop výkonu DC PV řízeného střídačem (pole A). EMS zapisuje jen pokud `ems.fn_site_has_active_green_bonus_pv(site_id)` (zelený bonus na PV poli) **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0`. Hodnota z aktivního `planning_interval`: bez curtailmentu = cap; s `pv_a_curtailed_w > 0` = `clamp(0, cap, pv_a_forecast_solver_w − pv_a_curtailed_w)`. **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN` — verify mismatch nečeká přepnutí do SELF_SUSTAIN. | +| 340 | Max solar power | min … cap (W) | 1 W | Strop výkonu DC PV řízeného střídačem (pole A). Cap z `fn_inverter_pv_a_max_w` (`deye_reg340_max_solar_w`, typ. **32 000** home-01, **65 000** větší hybridy), ne součet Wp — studené panely mohou překročit nominál. Min z `deye_reg340_min_solar_w` (home-01 **400 W**, jinde **0** dle firmware). EMS zapisuje jen při zeleném bonusu a cap > 0. **Není** v `DEYE_CRITICAL_REGS_SELF_SUSTAIN`. | | 143 | Export limit W | závisí na typu (SUN-20K až ~13 500) | 1 W | Max export do sítě; hodnota z `site_grid_connection.max_export_power_w`. EMS ji neodvozuje z forecastu ani z `grid_setpoint_w`; pro exportní sloty je to tvrdý site/inverter cap. | | 178 | Control board special 1 | bitmask | — | Bitové pole pro více funkcí. **EMS používá:** (a) bits **4–5** pro peak shaving switch: **32** (`0b00100000`, bit4–5 = **10**) v režimu **SELL**; **48** (`0b00110000`, bit4–5 = **11**) v **PASSIVE/CHARGE**. (b) **BA81:** bits **0–1** pro „MI export to Grid cutoff“ (AC coupling / GEN): **2** = disable (cutoff OFF), **3** = enable (cutoff ON). EMS zapisuje jako **read-modify-write** (zachová ostatní bity). V některých manuálech/UI je to označené jako „register 179“ (1-based). | | 190 | GEN peak shaving | 0–16000 | 1 W | Peak shaving na GEN portu | @@ -30,7 +30,7 @@ EMS zapisuje řídící hodnoty přes journal (`modbus_command`) a **`write_regi ### Reg 340 (max solar power) - **FC 0x10**, jednotka **W**; firmware omezuje strop výroby z řiditelných stringů (pole A na hybridu). -- **Kdy EMS zapisuje:** `ems.fn_site_has_active_green_bonus_pv(site_id)` **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (součet `nominal_power_wp` z `asset_pv_array` kde `controllable = true`). Při součtu **0** nebo bez aktivního zeleného bonusu EMS reg 340 **nezapisuje** (ruční hodnota v invertoru zůstane). +- **Kdy EMS zapisuje:** `ems.fn_site_has_active_green_bonus_pv(site_id)` **a** `ems.fn_inverter_pv_a_max_w(inverter_id) > 0` (řiditelné pole A + nenulový strop střídače z `deye_reg340_max_solar_w` / `max_dc_input_w`). Bez bonusu nebo cap **0** EMS reg 340 **nezapisuje**. - **Hodnota:** z `ControlSetpoints.pv_a_allowed_w` (AUTO): bez curtailmentu = plný cap; při `pv_a_curtailed_w > 0` viz tabulka výše. Režimy **SELF_SUSTAIN / PRESERVE / CHARGE_CHEAP** mají `pv_a_allowed_w = None` → žádný zápis 340 z EMS v daném ticku. - **Bez zápisu 340 (2026-05):** pokud plán má **bez exportu** (`export_mode = NONE` nebo `grid_setpoint_w ≥ 0` a `export_limit_w = 0`), **bez nabíjení baterie** (`battery_setpoint_w ≤ 0`) a **bez curtailu A** (`pv_a_curtailed_w = 0`), EMS reg 340 **neposílá** — Deye řídí PV A přes **108/109/142** (zero export + 0 A nabíjení). Funkce `plan_skips_deye_reg340_write` v `setpoints.py`. Výjimka: explicitní curtail nebo záporné buy+sell s PV B → `pv_a_allowed_w` se dopočítá / vynuluje jako dřív. - **Živé čtení:** `read_deye_registers_live` vrací **`reg340_max_solar_power_w`** (integer) jen pokud je přepínač zapnutý; jinak **`null`** (bez extra FC3 čtení reg 340). diff --git a/docs/planning-changelog.md b/docs/planning-changelog.md index bd17309..506240f 100644 --- a/docs/planning-changelog.md +++ b/docs/planning-changelog.md @@ -5,6 +5,14 @@ Formát: **datum (ISO)** · stručný důvod · soubory · chování / ověřen --- +## 2026-05-28 — reg 340 cap z výkonu střídače, min dle firmware (V082) + +**Změna:** `asset_inverter.deye_reg340_max_solar_w` / `deye_reg340_min_solar_w`; `fn_inverter_pv_a_max_w` bere strop z DB sloupce (home-01 **32 000 W**, ostatní Deye **65 000 W**), ne součet Wp polí — studené panely mohou překročit nominál. `compute_pv_a_reg340_max_solar_w(..., min_w=)` — spodní limit jen pro kladné hodnoty (home-01 min **400 W**). + +**Ověření:** `select ems.fn_inverter_pv_a_max_w();` · `pytest backend/tests/test_control_exporter_reg340.py`. + +--- + ## 2026-05-28 — reg 340 jen když plán curtailuje / exportuje / nabíjí **Změna:** `plan_skips_deye_reg340_write` v `setpoints.py` — bez FC 0x10 na reg **340**, pokud slot nemá export, nabíjení baterie ani `pv_a_curtailed_w` (Deye řídí PV A přes 108/109/142).