fix rizeni pole pres reg340 jen home01
Some checks failed
CI and deploy / migration-check (push) Failing after 12s
CI and deploy / deploy (push) Has been skipped

This commit is contained in:
Dusan Vojacek
2026-05-02 09:31:45 +02:00
parent ed88ef8910
commit fffe6c7185
9 changed files with 86 additions and 42 deletions

View File

@@ -203,9 +203,10 @@ class InverterConfig:
deye_tou_inactive_signature: str | None = None
deye_zero_export_mode: int = 1
deye_gen_microinverter_cutoff_enabled: bool = False
manufacturer: str = ""
#: Součet nominal_power_wp controllable PV na invertoru; 0 = EMS nezapisuje reg 340.
pv_a_cap_w: int = 0
#: True = EMS smí řídit Deye reg 340 (max solar power); z SQL `fn_site_has_active_green_bonus_pv(site_id)` — není DB sloupec na invertoru.
deye_reg340_pv_a_control_enabled: bool = False
def compute_pv_a_reg340_max_solar_w(cap_w: int, forecast_w: int, curtail_w: int) -> int:
@@ -1075,7 +1076,6 @@ async def _load_inverter_config(
"""
SELECT
ai.id, ai.code,
coalesce(ai.manufacturer, '') AS manufacturer,
coalesce(ems.fn_inverter_pv_a_max_w(ai.id), 0) AS pv_a_cap_w,
se.host, se.port, se.unit_id,
sgc.max_export_power_w,
@@ -1093,6 +1093,8 @@ async def _load_inverter_config(
ai.deye_tou_inactive_signature,
COALESCE(ai.deye_zero_export_mode, 1) AS deye_zero_export_mode,
COALESCE(ai.deye_gen_microinverter_cutoff_enabled, false) AS deye_gen_microinverter_cutoff_enabled,
coalesce(ems.fn_site_has_active_green_bonus_pv(ai.site_id), false)
AS deye_reg340_pv_a_control_enabled,
COALESCE(
ai.deye_register_max_charge_a,
FLOOR(
@@ -1177,8 +1179,10 @@ async def _load_inverter_config(
deye_tou_inactive_signature=row["deye_tou_inactive_signature"],
deye_zero_export_mode=int(row["deye_zero_export_mode"]),
deye_gen_microinverter_cutoff_enabled=bool(row["deye_gen_microinverter_cutoff_enabled"] or False),
manufacturer=str(row["manufacturer"] or ""),
pv_a_cap_w=int(row["pv_a_cap_w"] or 0),
deye_reg340_pv_a_control_enabled=bool(
row["deye_reg340_pv_a_control_enabled"] or False
),
)
@@ -1262,7 +1266,7 @@ def _build_setpoints(
pi: asyncpg.Record | None,
*,
pv_a_cap_w: int = 0,
inverter_manufacturer: str = "",
reg340_pv_a_control_enabled: bool = False,
) -> ControlSetpoints | None:
code = mode.mode_code
if code == "MANUAL":
@@ -1286,7 +1290,7 @@ def _build_setpoints(
gen_cutoff_raw = pi.get("deye_gen_cutoff_enabled")
gen_cutoff = bool(gen_cutoff_raw) if gen_cutoff_raw is not None else False
pv_a_allowed: int | None = None
if (inverter_manufacturer or "").strip().lower() == "deye" and int(pv_a_cap_w) > 0:
if bool(reg340_pv_a_control_enabled) and int(pv_a_cap_w) > 0:
forecast = int(pi.get("pv_a_forecast_solver_w") or 0)
curtail = int(pi.get("pv_a_curtailed_w") or 0)
pv_a_allowed = compute_pv_a_reg340_max_solar_w(int(pv_a_cap_w), forecast, curtail)
@@ -1677,9 +1681,8 @@ async def write_inverter_setpoints(
]
)
mfr = (inv.manufacturer or "").strip().lower()
if (
mfr == "deye"
bool(inv.deye_reg340_pv_a_control_enabled)
and int(inv.pv_a_cap_w) > 0
and setpoints_now.pv_a_allowed_w is not None
):
@@ -1826,7 +1829,8 @@ async def write_inverter_setpoints(
async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict[str, Any]:
"""
Živé čtení holding registrů Deye 108, 109, 141145, 178, 191, 340 (stejné TCP spojení jako telemetrie/export).
Živé čtení holding registrů Deye 108, 109, 141145, 178, 191 a volitelně 340
(jen pokud `deye_reg340_pv_a_control_enabled`, jinak `reg340_max_solar_power_w` = null).
Vše pod jedním mutexem + sdružené FC3 bloky — mezi jednotlivými read_register dřív telemetrie
střídavě brala lock a RS485 brány házely cizí transaction_id / I/O timeouty.
"""
@@ -1843,13 +1847,20 @@ async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict
b141 = await mb.read_holding_registers(141, 5)
r178 = await mb.read_holding_registers(178, 1)
r191 = await mb.read_holding_registers(191, 1)
r340 = await mb.read_holding_registers(340, 1)
if inv.deye_reg340_pv_a_control_enabled:
r340 = await mb.read_holding_registers(340, 1)
else:
r340 = None
r108, r109 = b108[0], b108[1]
r141, r142, r143 = b141[0], b141[1], b141[2]
r145 = b141[4]
r178 = r178[0]
r191 = r191[0]
r340v = int(r340[0]) if r340 and len(r340) >= 1 else 0
r340v = (
int(r340[0])
if r340 is not None and len(r340) >= 1
else None
)
except Exception:
logger.exception("read_deye_registers_live site=%s failed", site_id)
raise
@@ -1866,7 +1877,7 @@ async def read_deye_registers_live(site_id: int, db: asyncpg.Connection) -> dict
"reg178_mi_export_cutoff_bits": int(r178) & int(REG178_MI_EXPORT_MASK),
"reg178_mi_export_cutoff_is_on": (int(r178) & int(REG178_MI_EXPORT_MASK)) == int(REG178_MI_EXPORT_ENABLE),
"reg191_peak_shaving_w": int(r191),
"reg340_max_solar_power_w": int(r340v),
"reg340_max_solar_power_w": r340v,
"read_at": read_at.isoformat(),
}
@@ -2012,14 +2023,24 @@ 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
mfr_pv = (inv_for_pv.manufacturer or "") if inv_for_pv is not None else ""
reg340_en = (
bool(inv_for_pv.deye_reg340_pv_a_control_enabled)
if inv_for_pv is not None
else False
)
pi_now = await _fetch_plan_row_for_slot_offset(site_id, db, 0)
pi_next = await _fetch_plan_row_for_slot_offset(site_id, db, 1)
sp_now = _build_setpoints(
mode, pi_now, pv_a_cap_w=cap_pv, inverter_manufacturer=mfr_pv
mode,
pi_now,
pv_a_cap_w=cap_pv,
reg340_pv_a_control_enabled=reg340_en,
)
sp_next = _build_setpoints(
mode, pi_next, pv_a_cap_w=cap_pv, inverter_manufacturer=mfr_pv
mode,
pi_next,
pv_a_cap_w=cap_pv,
reg340_pv_a_control_enabled=reg340_en,
)
if mode.mode_code == "AUTO" and sp_now is None:

View File

@@ -53,12 +53,12 @@ class ComputePvAReg340Tests(unittest.TestCase):
class BuildSetpointsReg340Tests(unittest.TestCase):
def test_deye_with_cap_sets_pv_a_allowed(self) -> None:
def test_with_cap_sets_pv_a_allowed(self) -> None:
sp = _build_setpoints(
_auto_mode(),
_pi_base(pv_a_forecast_solver_w=8000, pv_a_curtailed_w=2000),
pv_a_cap_w=10_000,
inverter_manufacturer="Deye",
reg340_pv_a_control_enabled=True,
)
assert sp is not None
self.assertEqual(sp.pv_a_allowed_w, 6000)
@@ -68,17 +68,7 @@ class BuildSetpointsReg340Tests(unittest.TestCase):
_auto_mode(),
_pi_base(),
pv_a_cap_w=0,
inverter_manufacturer="Deye",
)
assert sp is not None
self.assertIsNone(sp.pv_a_allowed_w)
def test_skipped_for_non_deye(self) -> None:
sp = _build_setpoints(
_auto_mode(),
_pi_base(),
pv_a_cap_w=10_000,
inverter_manufacturer="Foo",
reg340_pv_a_control_enabled=True,
)
assert sp is not None
self.assertIsNone(sp.pv_a_allowed_w)
@@ -92,9 +82,7 @@ class BuildSetpointsReg340Tests(unittest.TestCase):
heat_pump_enabled_def=False,
loxone_mode_value=0,
)
sp = _build_setpoints(
mode, None, pv_a_cap_w=10_000, inverter_manufacturer="Deye"
)
sp = _build_setpoints(mode, None, pv_a_cap_w=10_000)
assert sp is not None
self.assertIsNone(sp.pv_a_allowed_w)
@@ -109,11 +97,21 @@ class BuildSetpointsReg340Tests(unittest.TestCase):
pv_a_curtailed_w=0,
),
pv_a_cap_w=3333,
inverter_manufacturer="Deye",
reg340_pv_a_control_enabled=True,
)
assert sp is not None
self.assertEqual(sp.pv_a_allowed_w, 0)
def test_skipped_when_reg340_control_disabled(self) -> None:
sp = _build_setpoints(
_auto_mode(),
_pi_base(pv_a_forecast_solver_w=8000, pv_a_curtailed_w=2000),
pv_a_cap_w=10_000,
reg340_pv_a_control_enabled=False,
)
assert sp is not None
self.assertIsNone(sp.pv_a_allowed_w)
class Reg340VerifyPolicyTests(unittest.TestCase):
def test_reg340_not_critical_for_self_sustain(self) -> None: