Files
EOS/tests/test_eosdashsettings.py
T

201 lines
8.0 KiB
Python
Raw Normal View History

"""Tests for the EOSdash settings in the EOS configuration (eosdash/plan)."""
from unittest.mock import MagicMock, patch
import pytest
from pydantic import ValidationError
import akkudoktoreos.server.dash.eosstatus as eosstatus
from akkudoktoreos.config.config import SettingsEOS
from akkudoktoreos.server.dash import plan
from akkudoktoreos.server.dash.dashsettings import (
EOSdashCommonSettings,
EOSdashPlanSettings,
)
SERVER = "http://127.0.0.1:8503"
class TestEOSdashPlanSettings:
def test_defaults(self):
settings = EOSdashPlanSettings()
assert settings.visible == {}
assert settings.color == {}
assert settings.device_alias == {}
assert settings.plan_table_open is False
assert settings.energy_kwh is True
assert settings.factor_percent is True
assert settings.price_minor_unit is True
assert EOSdashCommonSettings().plan.model_dump() == settings.model_dump()
def test_valid_values(self):
settings = EOSdashPlanSettings(
color={"battery1_soc_factor": "#DAA520"},
style={"pvforecast_ac_energy_wh": "bar"},
width={"loadforecast_energy_wh": 5},
line_dash={"mode_SELF_CONSUMPTION": "dotted"},
alias={"mode_SELF_CONSUMPTION": " Self consumption "},
)
assert settings.alias["mode_SELF_CONSUMPTION"] == "Self consumption"
@pytest.mark.parametrize(
"field, value",
[
("color", {"col": "red"}),
("color", {"col": "#12345"}),
("style", {"col": "area"}),
("width", {"col": 0}),
("width", {"col": 6}),
("line_dash", {"col": "dashdot"}),
("alias", {"col": ""}),
("alias", {"col": "x" * 33}),
("device_alias", {"battery1": " "}),
],
)
def test_invalid_values(self, field, value):
with pytest.raises(ValidationError):
EOSdashPlanSettings(**{field: value})
class TestEOSdashConfig:
def test_config_has_eosdash_section(self, config_eos):
assert config_eos.eosdash.plan.model_dump() == EOSdashPlanSettings().model_dump()
def test_set_nested_value(self, config_eos):
config_eos.set_nested_value("eosdash/plan/color", {"battery1_soc_factor": "#FF0000"})
config_eos.set_nested_value("eosdash/plan/price_minor_unit", False)
assert config_eos.eosdash.plan.color == {"battery1_soc_factor": "#FF0000"}
assert config_eos.eosdash.plan.price_minor_unit is False
with pytest.raises(ValueError):
config_eos.set_nested_value("eosdash/plan/color", {"battery1_soc_factor": "red"})
def test_merge_settings_from_dict(self, config_eos):
config_eos.merge_settings_from_dict(
{"eosdash": {"plan": {"device_alias": {"battery1": "House battery"}}}}
)
assert config_eos.eosdash.plan.device_alias == {"battery1": "House battery"}
# Other plan settings keep their defaults
assert config_eos.eosdash.plan.energy_kwh is True
@pytest.fixture
def eosdash_config():
"""EOSdash copy of the EOS configuration, restored after the test."""
saved = eosstatus.eos_config
eosstatus.eos_config = SettingsEOS(
eosdash=EOSdashCommonSettings(
plan=EOSdashPlanSettings(
color={"battery1_soc_factor": "#00FF00"},
alias={"mode_IDLE": "Idle"},
device_alias={"battery1": "House battery"},
price_minor_unit=False,
)
)
)
yield eosstatus.eos_config
eosstatus.eos_config = saved
def put_response(config: SettingsEOS) -> MagicMock:
"""Response of EOS to a configuration update."""
response = MagicMock()
response.json.return_value = config.model_dump(mode="json")
return response
class TestPlanSettings:
def test_defaults_without_config(self):
saved = eosstatus.eos_config
eosstatus.eos_config = None
try:
assert plan.plan_settings().model_dump() == EOSdashPlanSettings().model_dump()
assert plan.plan_option("price_minor_unit") is True
finally:
eosstatus.eos_config = saved
def test_settings_from_config(self, eosdash_config):
assert plan.solution_column_color("battery1_soc_factor") == "#00FF00"
assert plan.plan_mode_label("IDLE") == "Idle"
assert plan.device_alias("battery1") == "House battery"
assert plan.device_alias("ev11") == "ev11"
assert plan.column_label("battery1_soc_factor", ["battery1"]) == "House battery soc_factor"
assert plan.plan_option("price_minor_unit") is False
assert plan.plan_option("energy_kwh") is True
def test_put_replaces_config(self, eosdash_config):
updated = eosdash_config.model_copy(deep=True)
updated.eosdash.plan.price_minor_unit = True
with patch.object(plan.requests, "put", return_value=put_response(updated)) as put:
plan.plan_settings_put(SERVER, "price_minor_unit", True)
put.assert_called_once_with(
f"{SERVER}/v1/config/eosdash/plan/price_minor_unit", json=True, timeout=10
)
assert plan.plan_option("price_minor_unit") is True
def test_put_failure_keeps_config(self, eosdash_config):
with patch.object(plan.requests, "put", side_effect=OSError("no connection")):
plan.plan_settings_put(SERVER, "price_minor_unit", True)
assert plan.plan_option("price_minor_unit") is False
@pytest.mark.parametrize(
"data, key, value",
[
(
{"action": "color", "renderer": "load", "color": "#abcdef"},
"color",
{"battery1_soc_factor": "#00FF00", "load": "#ABCDEF"},
),
({"action": "reset", "renderer": "battery1_soc_factor"}, "color", {}),
({"action": "visible", "renderer": "load", "load-visible": "true"}, "visible", {"load": True}),
({"action": "visible", "renderer": "load"}, "visible", {"load": False}),
({"action": "style", "renderer": "load", "style": "bar"}, "style", {"load": "bar"}),
({"action": "width", "renderer": "load", "width": "3"}, "width", {"load": 3}),
({"action": "dash", "renderer": "load", "dash": "dotted"}, "line_dash", {"load": "dotted"}),
(
{"action": "alias", "renderer": "load", "alias": " House load\x00 "},
"alias",
{"mode_IDLE": "Idle", "load": "House load"},
),
({"action": "alias", "renderer": "mode_IDLE", "alias": ""}, "alias", {}),
],
)
def test_solution_settings_action(self, eosdash_config, data, key, value):
with patch.object(plan, "plan_settings_put") as put:
plan.solution_settings_action(SERVER, data)
put.assert_called_once_with(SERVER, key, value)
@pytest.mark.parametrize(
"data",
[
{"action": "color", "renderer": "load", "color": "red"},
{"action": "style", "renderer": "load", "style": "area"},
{"action": "width", "renderer": "load", "width": "9"},
{"action": "dash", "renderer": "load", "dash": "dashdot"},
{"action": "color", "color": "#ABCDEF"},
],
)
def test_solution_settings_action_invalid(self, eosdash_config, data):
with patch.object(plan, "plan_settings_put") as put:
plan.solution_settings_action(SERVER, data)
put.assert_not_called()
class TestCurrencyMinorUnit:
@pytest.mark.parametrize(
"currency, expected",
[
("EUR", ("ct", 100)),
("CHF", ("Rp.", 100)),
("SEK", ("1/100 SEK", 100)),
("KWD", ("1/1000 KWD", 1000)),
("JPY", None),
(None, None),
],
)
def test_currency_minor_unit(self, currency, expected):
assert plan.currency_minor_unit(currency) == expected
def test_config_currency(self):
config = SettingsEOS.model_validate({"general": {"timezone_override": "Europe/Zurich"}})
assert plan.config_currency(config) == "CHF"