Files
ems/backend/tests/test_control_exporter_tou.py
Dusan Vojacek f8e1eed127
All checks were successful
CI and deploy / migration-check (push) Successful in 6s
CI and deploy / deploy (push) Successful in 29s
fix rs485 s eror self_sustain
2026-04-19 15:29:58 +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 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()