Files
ems/backend/tests/test_control_exporter_tou.py
Dusan Vojacek 93f883f5e0
Some checks failed
CI and deploy / migration-check (push) Successful in 5s
CI and deploy / deploy (push) Failing after 20s
sql first refactor
2026-04-19 20:02:20 +02:00

194 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Deye TOU SOC % podle fyzického režimu (SELL vs PASSIVE)."""
from __future__ import annotations
import unittest
from dataclasses import replace
from services.control.exporter_monolith import (
ControlSetpoints,
InverterConfig,
_deye_reg178_verify_with_double_read,
_deye_tou_params,
_deye_tou_power_verify_match,
deye_reg_triggers_self_sustain_after_verify_exhaust,
get_deye_mode,
)
def _inv(*, min_soc: int | None = 12, reserve_soc: int | None = 20) -> InverterConfig:
return InverterConfig(
id=1,
code="deye-main",
host="127.0.0.1",
port=502,
unit_id=1,
max_export_power_w=13_500,
max_import_power_w=13_500,
no_export=False,
max_battery_charge_w=10_000,
max_battery_discharge_w=10_000,
min_soc_percent=min_soc,
reserve_soc_percent=reserve_soc,
max_soc_percent=95,
usable_capacity_wh=64_000,
max_charge_a=100,
max_discharge_a=100,
)
def _inv_350a() -> InverterConfig:
"""350 A × 51.2 V = 17920 W — typický firmware clamp pro TOU power."""
return replace(_inv(), max_charge_a=350, max_discharge_a=350)
class ModbusVerifyPolicyTests(unittest.TestCase):
def test_tou_power_accepts_firmware_max_w_clamp(self) -> None:
inv = _inv_350a()
self.assertTrue(_deye_tou_power_verify_match(7752, 17920, inv))
self.assertTrue(_deye_tou_power_verify_match(16728, 17920, inv))
def test_reg178_double_read_recovers_from_glitch(self) -> None:
ok, v = _deye_reg178_verify_with_double_read(48, 12014, 48)
self.assertTrue(ok)
self.assertEqual(v, 48)
def test_reg178_not_critical_for_self_sustain(self) -> None:
self.assertFalse(deye_reg_triggers_self_sustain_after_verify_exhaust(178))
def test_reg108_critical_for_self_sustain(self) -> None:
self.assertTrue(deye_reg_triggers_self_sustain_after_verify_exhaust(108))
class DeyeTouParamsTests(unittest.TestCase):
def test_sell_uses_reserve_soc(self) -> None:
sp = ControlSetpoints(
battery_w=-600,
grid_export_limit=5000,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=-500,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=50,
)
self.assertEqual(get_deye_mode(sp), "SELL")
p, s, g = _deye_tou_params(sp, _inv())
self.assertFalse(g)
self.assertEqual(s, 20)
def test_passive_uses_min_soc(self) -> None:
sp = ControlSetpoints(
battery_w=0,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=0,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=None,
effective_sell_price_czk_kwh=None,
)
self.assertEqual(get_deye_mode(sp), "PASSIVE")
p, s, g = _deye_tou_params(sp, _inv(min_soc=12, reserve_soc=20))
self.assertFalse(g)
self.assertEqual(s, 12)
def test_passive_negative_sell_steers_tou_above_current_soc(self) -> None:
"""Záporná vykupní → TOU SOC = 100 % (priorita akumulace vs. přetok)."""
sp = ControlSetpoints(
battery_w=-400,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=0,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=14,
effective_sell_price_czk_kwh=-0.25,
)
self.assertEqual(get_deye_mode(sp), "PASSIVE")
_p, s, g = _deye_tou_params(sp, _inv(min_soc=12, reserve_soc=20))
self.assertFalse(g)
self.assertEqual(s, 100)
def test_passive_planned_charge_steers_tou(self) -> None:
sp = ControlSetpoints(
battery_w=800,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=0,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=60,
effective_sell_price_czk_kwh=1.0,
)
self.assertEqual(get_deye_mode(sp), "PASSIVE")
_p, s, g = _deye_tou_params(sp, _inv(min_soc=12, reserve_soc=20))
self.assertFalse(g)
self.assertEqual(s, 100)
def test_charge_unchanged_grid_charge(self) -> None:
sp = ControlSetpoints(
battery_w=5000,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=5000,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=80,
)
self.assertEqual(get_deye_mode(sp), "CHARGE")
_p, s, g = _deye_tou_params(sp, _inv())
self.assertTrue(g)
self.assertEqual(s, 95)
def test_self_sustain_tou_stays_min_soc_even_if_sell_negative(self) -> None:
"""SELF_SUSTAIN: nízké TOU (min_soc), ne 100 % z negativní vykupní — LP se nepoužívá."""
sp = ControlSetpoints(
battery_w=None,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=0,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=None,
effective_sell_price_czk_kwh=-0.48,
self_sustain_local_use=True,
)
self.assertEqual(get_deye_mode(sp), "PASSIVE")
_p, s, g = _deye_tou_params(sp, _inv(min_soc=12, reserve_soc=20))
self.assertFalse(g)
self.assertEqual(s, 12)
def test_lock_battery_uses_min_soc(self) -> None:
sp = ControlSetpoints(
battery_w=0,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=-500,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=None,
lock_battery=True,
)
p, s, g = _deye_tou_params(sp, _inv(min_soc=12))
self.assertEqual(p, 0)
self.assertFalse(g)
self.assertEqual(s, 12)
if __name__ == "__main__":
unittest.main()