Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion OWNd/message/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand Down
54 changes: 54 additions & 0 deletions tests/test_who0_scenario_module_states.py
Original file line number Diff line number Diff line change
@@ -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
Loading