- Introduced `effective_sell_price_czk_kwh` to `ControlSetpoints` for managing battery usage based on sell price. - Implemented logic in `_deye_passive_tou_battery_soc_pct` to set TOU SOC to 100% when conditions favor battery usage. - Updated tests to validate new behavior for negative sell prices and planned charging scenarios. - Enhanced documentation to clarify TOU SOC behavior in passive mode.
147 lines
4.4 KiB
Python
147 lines
4.4 KiB
Python
"""Deye TOU SOC % podle fyzického režimu (SELL vs PASSIVE)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from services.control_exporter import (
|
|
ControlSetpoints,
|
|
InverterConfig,
|
|
_deye_tou_params,
|
|
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,
|
|
)
|
|
|
|
|
|
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_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()
|