solver nastavuje stavy deye
Some checks failed
CI and deploy / migration-check (push) Failing after 14s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-04-20 08:33:56 +02:00
parent 6447666cee
commit 43b594c8d5
10 changed files with 219 additions and 70 deletions

View File

@@ -32,7 +32,7 @@ DEYE_CLOCK_RESYNC_INTERVAL_HOURS = 24
# Deye LV baterie: převod výkon → proud pro registry 108/109 (viz docs/04-modules/modbus-registers.md)
BATT_VOLTAGE_V = 51.2
# Reg 143 ve SELL: strop exportu = min(site max, |grid_setpoint|), dolní podlahová konstanta kvůli nule.
# Reg 143 ve SELL: min(|grid_setpoint_w|, …) nesmí klesnout pod tuto podlahu (W) — kvůli chování firmware, ne mapování režimu.
REG143_SELL_CAP_MIN_W = 200
# Reg 178 pevné hodnoty (bit45); bez read-modify-write (kolize s Loxone / transaction ID)
@@ -340,6 +340,8 @@ class ControlSetpoints:
ev1_power_w: int
ev2_power_w: int
target_soc_pct: int | None = None
#: Explicitní fyzický režim z plánu (PASSIVE/SELL/CHARGE). Pokud je vyplněn, má přednost před detekcí ze znamének.
deye_physical_mode: str | None = None
#: Efektivní vykupní cena slotu (Kč/kWh z plánu); pro TOU řízení priorit baterie vs. přetok
effective_sell_price_czk_kwh: float | None = None
#: True = reg 108/109 na 0 (PRESERVE Deye baterii nepoužívá)
@@ -1220,6 +1222,8 @@ def _build_setpoints(mode: OperatingModeInfo, pi: asyncpg.Record | None) -> Cont
hp_en = bool(pi["heat_pump_enabled"])
tgt = pi["battery_soc_target_pct"]
target_soc = int(round(float(tgt))) if tgt is not None else None
pm_raw = pi.get("deye_physical_mode")
pm: str | None = str(pm_raw).strip().upper() if pm_raw is not None else None
sell_raw = pi.get("effective_sell_price")
sell_f: float | None = float(sell_raw) if sell_raw is not None else None
return ControlSetpoints(
@@ -1232,6 +1236,7 @@ def _build_setpoints(mode: OperatingModeInfo, pi: asyncpg.Record | None) -> Cont
ev1_power_w=ev1_w,
ev2_power_w=ev2_w,
target_soc_pct=target_soc,
deye_physical_mode=pm,
effective_sell_price_czk_kwh=sell_f,
)
@@ -1373,31 +1378,52 @@ def _deye_passive_tou_battery_soc_pct(
return _clamp_deye_tou_soc_pct_hi(DEYE_TOU_SOC_PASSIVE_BATTERY_PRIORITY_PCT, hi=100)
def _deye_zero_export_amps_for_passive(
grid_w: int,
bat_w: int,
max_charge_a: int,
max_discharge_a: int,
) -> tuple[int, int]:
"""
PASSIVE (zero export k CT/zátěži, reg. 142 dle DB): výchozí plné 108/109.
- Export v plánu (grid_w < 0) a žádné plánované vybíjení (bat_w >= 0): **108 = 0** — nepřebírat
přebytek FVE do baterie, ať může jít přetok do sítě.
- Import v plánu (grid_w > 0) a žádné plánované nabíjení (bat_w <= 0): **109 = 0** — nevybíjet
baterii, odběr ze sítě.
"""
if grid_w < 0 and bat_w >= 0:
return 0, max_discharge_a
if grid_w > 0 and bat_w <= 0:
return max_charge_a, 0
return max_charge_a, max_discharge_a
def get_deye_mode(setpoints: ControlSetpoints) -> str:
"""
Fyzický režim Deye: SELL | CHARGE | PASSIVE.
Pravidlo držet jednoduché (viz ``docs/04-modules/operating-modes.md``):
Primárně explicitně z plánu (`setpoints.deye_physical_mode`), fallback jen ze znamének (viz
``docs/04-modules/operating-modes.md``):
- **SELL** — jen když plán explicitně počítá s **vybíjením baterie do sítě** ve smyslu:
záporný export z portu sítě a zároveň **|battery_w| ≥ |grid_setpoint_w|** (výdej z
baterie není menší než plánovaný čistý export). Pak Deye „selling first“ (reg. 142=0).
- **CHARGE** — ``battery_w`` > 0 **a** ``grid_setpoint_w`` > 0 (nabíjení ze sítě + import v plánu).
- **SELL** — ``grid_setpoint_w`` < 0 **a** ``battery_w`` < 0 (export + vybíjení baterie v plánu).
- **PASSIVE** (ZERO) — vše ostatní; reg. **108/109** dle ``_deye_zero_export_amps_for_passive``.
- **PASSIVE** — všude jinde: reg. **108** / **109** na **max. proud** invertoru; přetok FVE / chování
vůči zátěži drží **142** dle ``deye_zero_export_mode``, **TOU výkon** z plánu, **145** solar sell.
- **CHARGE** — nabíjení ze sítě (``battery_w`` > 500 a ``grid_setpoint_w`` > 200).
``battery_w=None`` (SELF_SUSTAIN) → bat_w 0 → PASSIVE zde; **108/109 max** stejně jako u běžného
PASSIVE v ``write_inverter_setpoints`` (viz ``self_sustain_local_use`` pro TOU SOC).
``battery_w=None`` (SELF_SUSTAIN) → bat_w 0 → typicky PASSIVE; v ``write_inverter_setpoints`` má
SELF_SUSTAIN vlastní větev (108/109 max).
"""
pm = (setpoints.deye_physical_mode or "").strip().upper()
if pm in {"PASSIVE", "SELL", "CHARGE"}:
return pm
grid_w = int(setpoints.grid_setpoint_w or 0)
bat_w = 0 if setpoints.battery_w is None else int(setpoints.battery_w)
if bat_w > 500 and grid_w > 200:
if bat_w > 0 and grid_w > 0:
return "CHARGE"
if grid_w < 0 and bat_w < 0 and abs(bat_w) >= abs(grid_w):
if grid_w < 0 and bat_w < 0:
return "SELL"
return "PASSIVE"
@@ -1475,18 +1501,19 @@ async def write_inverter_setpoints(
charge_a = int(inv.max_charge_a)
discharge_a = int(inv.max_discharge_a)
else:
# PASSIVE (AUTO): plný strop 108/109 — stejná idea jako SELF_SUSTAIN.
# Dříve škálování podle |battery_w| z LP usekávalo fyzický výkon baterie (např. 23 A při
# ~1,2 kW plánu) a velká akumulace pak neuměla rychle doplnit síť při nárazové zátěži.
# Ekonomiku a směr toku drží TOU časové body (výkon W / SOC %) + režim 142/178, ne reg. 108/109.
charge_a = int(inv.max_charge_a)
discharge_a = int(inv.max_discharge_a)
# PASSIVE (ZERO): výchozí plné 108/109; u přetoku FVE do sítě nebo importu bez baterie viz helper.
charge_a, discharge_a = _deye_zero_export_amps_for_passive(
grid_w,
bat_w,
int(inv.max_charge_a),
int(inv.max_discharge_a),
)
zero_exp_mode = int(inv.deye_zero_export_mode or 1)
selling_mode = 0 if deye_mode == "SELL" else zero_exp_mode
solar_sell = 1
export_limit = export_lim
if deye_mode == "SELL" and grid_w < -200:
if deye_mode == "SELL" and grid_w < 0:
export_limit = min(export_lim, max(REG143_SELL_CAP_MIN_W, abs(grid_w)))
reg178_val = REG178_SELL if deye_mode == "SELL" else REG178_PASSIVE
@@ -1875,15 +1902,15 @@ async def export_setpoints(site_id: int, db: asyncpg.Connection) -> None:
if mode.mode_code == "CHARGE_CHEAP":
max_ch = await _fetch_max_charge_power_w(site_id, db)
# Kladný grid_setpoint_w > 200 → fyzický CHARGE (nabíjení ze sítě), viz get_deye_mode
grid_for_charge = max(300, max_ch)
# Oba setpointy kladné → get_deye_mode CHARGE; min. 1 W, aby režim nebyl PASSIVE při nulové DB.
pw = max(1, int(max_ch))
sp_now = ControlSetpoints(
battery_w=max_ch,
battery_w=pw,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=grid_for_charge,
grid_setpoint_w=pw,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=None,