diff --git a/OWNd/message/scenario.py b/OWNd/message/scenario.py index 4c1f0aa..4f8242e 100644 --- a/OWNd/message/scenario.py +++ b/OWNd/message/scenario.py @@ -13,7 +13,41 @@ def __init__(self, data: str) -> None: self._scenario = self._what self._control_panel = self._where - self._human_readable_log = f"Scenario {self._scenario} from control panel {self._control_panel} has been launched." # pylint: disable=line-too-long + self._programming_scenario: int | None = None + self._event: str | None = None + # WHAT 40-46 are scenario-module programming states, not scenarios + # (libqtdevices scenario_device.cpp:27-35, 93-156; mhs1 bt_luci + # accepts *0*40#N*W##, 41#N, 42, 42#N, 43, 44 and emits 40#N..46#N). + _programming = { + 40: "programming started", + 41: "programming stopped", + 42: "deleted", + 43: "locked", + 44: "unlocked", + 45: "unavailable", + 46: "memory full", + } + if self._what is None: + # Status request (*#0*W##, *#0##): no scenario and no event, as + # OWNAuxEvent does for a bare *#9##. + self._scenario = None + elif self._what in _programming: + self._event = _programming[self._what] + self._scenario = None + # The grammar only admits digits in a WHAT parameter, so int() + # cannot fail here. + self._programming_scenario = ( + int(self._what_param[0]) if self._what_param else None + ) + _target = ( + f"scenario {self._programming_scenario}" + if self._programming_scenario is not None + else "all scenarios" + ) + self._human_readable_log = f"Scenario module {self._control_panel}: {_target} {self._event}." # pylint: disable=line-too-long + else: + self._event = "launched" + self._human_readable_log = f"Scenario {self._scenario} from control panel {self._control_panel} has been launched." # pylint: disable=line-too-long @property def scenario(self) -> int | None: @@ -23,6 +57,16 @@ def scenario(self) -> int | None: def control_panel(self) -> str | None: return self._control_panel + @property + def event(self) -> str | None: + """'launched' for WHAT 1-31, the programming/lock state for WHAT 40-46, None for a status request.""" + return self._event + + @property + def programming_scenario(self) -> int | None: + """Scenario named by a WHAT 40/41/42 parameter, None when it targets all.""" + return self._programming_scenario + class OWNAuxEvent(OWNEvent): diff --git a/tests/test_who0_scenario_module_states.py b/tests/test_who0_scenario_module_states.py new file mode 100644 index 0000000..80e3099 --- /dev/null +++ b/tests/test_who0_scenario_module_states.py @@ -0,0 +1,54 @@ +"""WHO 0 WHAT 40-46 are scenario-module programming / lock states, not scenarios. + +Evidence: libqtdevices TS10_1_0_23 scenario_device.cpp:27-34 (START_PROG = 40, +STOP_PROG = 41, DELETE = 42, LOCK = 43, UNLOCK = 44) and :93-156 (parseFrame); +firmware replay on bt_luci (MyHomeServer1, F454, F459): *0*40#1*W## ACK bus +11 00 1C 41, *0*41#1*W## 1C 61, *0*42*W## 1C 00, *0*42#1*W## 1C 21, *0*43*W## +1C FF, *0*44*W## 1C E0; emitted by the firmware for bus frames: *0*42*31##, +*0*43*31##, *0*45*99##, *0*46#9*99##. +""" + +import pytest + +from OWNd.message import OWNEvent, OWNScenarioEvent + + +@pytest.mark.parametrize( + ("frame", "event", "target"), + [ + ("*0*40#1*11##", "programming started", 1), + ("*0*41#1*11##", "programming stopped", 1), + ("*0*42*31##", "deleted", None), + ("*0*42#1*11##", "deleted", 1), + ("*0*43*31##", "locked", None), + ("*0*44*11##", "unlocked", None), + ("*0*45*99##", "unavailable", None), + ("*0*46#9*99##", "memory full", 9), + ], +) +def test_programming_and_lock_states(frame: str, event: str, target: int | None): + msg = OWNEvent.parse(frame) + assert isinstance(msg, OWNScenarioEvent) + assert msg.event == event + assert msg.programming_scenario == target + assert msg.scenario is None + assert "launched" not in msg.human_readable_log + + +def test_plain_scenario_is_still_launched(): + msg = OWNEvent.parse("*0*3*11##") + assert isinstance(msg, OWNScenarioEvent) + assert msg.scenario == 3 + assert msg.event == "launched" + assert msg.programming_scenario is None + assert msg.human_readable_log == "Scenario 3 from control panel 11 has been launched." + + +@pytest.mark.parametrize("frame", ["*#0*11##", "*#0##"]) +def test_status_request_is_not_a_launch(frame: str): + msg = OWNEvent.parse(frame) + assert isinstance(msg, OWNScenarioEvent) + assert msg.scenario is None + assert msg.event is None + assert msg.programming_scenario is None + assert "launched" not in msg.human_readable_log