Add support for inverter current caps in site configuration
Some checks failed
deploy / deploy (push) Failing after 55s
test / smoke-test (push) Successful in 3s

- Introduced `InverterModbusCurrentCapsBody` model for updating max charge and discharge currents.
- Updated SQL queries to utilize `COALESCE` for effective current limits.
- Modified relevant tests to reflect changes in battery current handling.
- Added new SQL migration for `deye_register_max_current_a` columns in the database.
This commit is contained in:
Dusan Vojacek
2026-04-19 12:10:37 +02:00
parent fd06811753
commit a1aa6acf61
7 changed files with 121 additions and 56 deletions

View File

@@ -93,19 +93,12 @@ def watts_to_amps(power_w: int | None, phases: int = 3, voltage: int = 230) -> i
def battery_watts_to_amps(power_w: int, max_amps: int) -> int:
"""Proud z |výkonu| baterie; max_amps výhradně z DB (_load_inverter_config)."""
return min(max(0, max_amps), max(0, round(abs(power_w) / BATT_VOLTAGE_V)))
"""Proud z |výkonu| baterie; max_amps z DB (už COALESCE se stropy v SQL).
def _effective_battery_current_caps(inv: InverterConfig) -> tuple[int, int]:
"""Efektivní stropy pro reg 108/109 po zohlednění volitelných Deye limitů v DB."""
ca = int(inv.max_charge_a)
da = int(inv.max_discharge_a)
if inv.deye_register_max_charge_a is not None:
ca = min(ca, int(inv.deye_register_max_charge_a))
if inv.deye_register_max_discharge_a is not None:
da = min(da, int(inv.deye_register_max_discharge_a))
return ca, da
int(|W|/51.2) — u kladných hodnot stejné jako floor bez importu math.
"""
derived = int(abs(power_w) / BATT_VOLTAGE_V)
return min(max(0, max_amps), max(0, derived))
def current_slot_hhmm() -> int:
@@ -147,8 +140,6 @@ class InverterConfig:
usable_capacity_wh: int | None
max_charge_a: int
max_discharge_a: int
deye_register_max_charge_a: int | None = None
deye_register_max_discharge_a: int | None = None
deye_last_system_time_sync_minute: datetime | None = None
deye_last_system_time_sync_at: datetime | None = None
deye_last_tou_inactive_write_prague_date: date | None = None
@@ -972,17 +963,25 @@ async def _load_inverter_config(
ai.deye_last_system_time_sync_at,
ai.deye_last_tou_inactive_write_prague_date,
ai.deye_tou_inactive_signature,
ai.deye_register_max_charge_a,
ai.deye_register_max_discharge_a,
COALESCE(ai.deye_zero_export_mode, 1) AS deye_zero_export_mode,
LEAST(
COALESCE(ab.bms_max_charge_w, ai.max_battery_charge_w),
ai.max_battery_charge_w
) / 51.2 AS max_charge_a,
LEAST(
COALESCE(ab.bms_max_discharge_w, ai.max_battery_discharge_w),
ai.max_battery_discharge_w
) / 51.2 AS max_discharge_a
COALESCE(
ai.deye_register_max_charge_a,
FLOOR(
LEAST(
COALESCE(ab.bms_max_charge_w, ai.max_battery_charge_w),
ai.max_battery_charge_w
)::numeric / 51.2
)::int
) AS max_charge_a,
COALESCE(
ai.deye_register_max_discharge_a,
FLOOR(
LEAST(
COALESCE(ab.bms_max_discharge_w, ai.max_battery_discharge_w),
ai.max_battery_discharge_w
)::numeric / 51.2
)::int
) AS max_discharge_a
FROM ems.asset_inverter ai
JOIN ems.site_endpoint se ON se.id = ai.endpoint_id
JOIN ems.asset_battery ab ON ab.inverter_id = ai.id
@@ -1038,12 +1037,6 @@ async def _load_inverter_config(
else None,
max_charge_a=max_charge_a,
max_discharge_a=max_discharge_a,
deye_register_max_charge_a=int(row["deye_register_max_charge_a"])
if row["deye_register_max_charge_a"] is not None
else None,
deye_register_max_discharge_a=int(row["deye_register_max_discharge_a"])
if row["deye_register_max_discharge_a"] is not None
else None,
deye_last_system_time_sync_minute=row["deye_last_system_time_sync_minute"],
deye_last_system_time_sync_at=row["deye_last_system_time_sync_at"],
deye_last_tou_inactive_write_prague_date=row[
@@ -1288,17 +1281,12 @@ def get_deye_mode(setpoints: ControlSetpoints) -> str:
def _deye_tou_params(
setpoints: ControlSetpoints,
inv: InverterConfig,
*,
max_charge_a_cap: int | None = None,
max_discharge_a_cap: int | None = None,
) -> tuple[int, int, bool]:
"""
Parametry jednoho Deye time pointu: výkon W, SOC min %, grid_charge.
Musí odpovídat logice get_deye_mode / lock_battery v write_inverter_setpoints.
"""
md = int(max_discharge_a_cap) if max_discharge_a_cap is not None else int(inv.max_discharge_a)
mc = int(max_charge_a_cap) if max_charge_a_cap is not None else int(inv.max_charge_a)
max_batt_w_discharge = int(md * BATT_VOLTAGE_V)
max_batt_w_discharge = int(inv.max_discharge_a * BATT_VOLTAGE_V)
tp_discharge_w = 0 if setpoints.lock_battery else max_batt_w_discharge
tou_min = _deye_tou_min_soc_pct(inv)
tou_reserve = _deye_tou_reserve_soc_pct(inv)
@@ -1310,7 +1298,9 @@ def _deye_tou_params(
battery_w = int(raw_bat) if raw_bat is not None else 0
cap = int(inv.max_soc_percent) if inv.max_soc_percent is not None else 95
target_soc = max(10, min(95, cap))
tp_charge_w = battery_watts_to_amps(battery_w, mc) * int(BATT_VOLTAGE_V)
tp_charge_w = (
battery_watts_to_amps(battery_w, int(inv.max_charge_a)) * int(BATT_VOLTAGE_V)
)
return tp_charge_w, target_soc, True
if deye_mode == "SELL":
return tp_discharge_w, tou_reserve, False
@@ -1332,8 +1322,7 @@ async def write_inverter_setpoints(
grid_w = int(setpoints_now.grid_setpoint_w or 0)
no_export = inv.no_export
export_lim = _deye_reg143_export_w(no_export, inv.max_export_power_w)
eff_ca, eff_da = _effective_battery_current_caps(inv)
max_batt_w_discharge = int(eff_da * BATT_VOLTAGE_V)
max_batt_w_discharge = int(inv.max_discharge_a * BATT_VOLTAGE_V)
tp_discharge_w = 0 if setpoints_now.lock_battery else max_batt_w_discharge
tou_min_pct = _deye_tou_min_soc_pct(inv)
tou_reserve_pct = _deye_tou_reserve_soc_pct(inv)
@@ -1348,11 +1337,11 @@ async def write_inverter_setpoints(
charge_a = 0
discharge_a = 0
elif deye_mode == "CHARGE":
charge_a = battery_watts_to_amps(bat_w, eff_ca)
charge_a = battery_watts_to_amps(bat_w, inv.max_charge_a)
discharge_a = 0
else:
charge_a = int(eff_ca) if bat_w > 0 else 0
discharge_a = int(eff_da)
charge_a = int(inv.max_charge_a) if bat_w > 0 else 0
discharge_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
@@ -1401,12 +1390,8 @@ async def write_inverter_setpoints(
sp_tp2 = setpoints_next if setpoints_next is not None else setpoints_now
hh_cur = current_slot_hhmm()
hh_nxt = next_slot_hhmm()
p1, s1, g1 = _deye_tou_params(
setpoints_now, inv, max_charge_a_cap=eff_ca, max_discharge_a_cap=eff_da
)
p2, s2, g2 = _deye_tou_params(
sp_tp2, inv, max_charge_a_cap=eff_ca, max_discharge_a_cap=eff_da
)
p1, s1, g1 = _deye_tou_params(setpoints_now, inv)
p2, s2, g2 = _deye_tou_params(sp_tp2, inv)
registers.extend(_deye_time_point_rows(0, hh_cur, p1, s1, g1))
registers.extend(_deye_time_point_rows(1, hh_nxt, p2, s2, g2))