nastavitelny max sollar dle stridace (ulozeno v DB)
Some checks failed
CI and deploy / migration-check (push) Failing after 13s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-25 11:25:29 +02:00
parent e06f76b9ff
commit c6074e9c74
13 changed files with 101 additions and 17 deletions

View File

@@ -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 **6264** (č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ů 60499:** 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 10100 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 6264**, bloky TOU **12** vs **36**, 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ů 60499:** 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 10100 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 6264**, bloky TOU **12** vs **36**, 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 1112 %, migrace V029 + komentář sloupce).

View File

@@ -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:

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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
),

View File

@@ -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,

View File

@@ -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:

View File

@@ -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;

View File

@@ -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
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.';

View File

@@ -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 (

View File

@@ -144,7 +144,7 @@ bits 01). 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).

View File

@@ -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 **45** pro peak shaving switch: **32** (`0b00100000`, bit45 = **10**) v režimu **SELL**; **48** (`0b00110000`, bit45 = **11**) v **PASSIVE/CHARGE**. (b) **BA81:** bits **01** 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 | 016000 | 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).

View File

@@ -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(<deye-main id>);` · `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).