Skip to content

Commit 53d5eff

Browse files
committed
Review improvements
1 parent 6741a2c commit 53d5eff

3 files changed

Lines changed: 74 additions & 3 deletions

File tree

paradox/hardware/evo/panel.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,8 @@ async def control_module_pgm_outputs(self, module_address: int, pgm_index: int,
316316
:param str command: textual command
317317
:return: True if accepted
318318
"""
319-
pgm_commands = ["release"] * parsers.MODULE_PGM_OUTPUTS_PER_MODULE
319+
assert 1 <= pgm_index <= parsers.MODULE_PGM_PACKET_SLOTS, "pgm_index must be between 1 and %d" % parsers.MODULE_PGM_PACKET_SLOTS
320+
pgm_commands = ["release"] * parsers.MODULE_PGM_PACKET_SLOTS
320321
pgm_commands[pgm_index - 1] = command
321322

322323
args = {"module_address": module_address, "pgm_commands": pgm_commands}

paradox/hardware/evo/parsers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,9 @@ def _parse(self, stream, context, path):
823823
"checksum" / PacketChecksum(Bytes(1)),
824824
)
825825

826-
MODULE_PGM_OUTPUTS_PER_MODULE = 4
826+
# Fixed number of PGM command slots in the 0xA4 packet — a protocol constant,
827+
# not the number of PGMs a specific module exposes (which is configured separately).
828+
MODULE_PGM_PACKET_SLOTS = 4
827829

828830
PerformModulePGMAction = Struct(
829831
"fields"
@@ -834,7 +836,7 @@ def _parse(self, stream, context, path):
834836
"_not_used0" / Padding(1),
835837
"module_address" / Default(Int8ub, 0),
836838
"_not_used1" / Padding(2),
837-
"pgm_commands" / Array(MODULE_PGM_OUTPUTS_PER_MODULE, Default(_PGMCommandEnum, "release")),
839+
"pgm_commands" / Array(MODULE_PGM_PACKET_SLOTS, Default(_PGMCommandEnum, "release")),
838840
"_not_used2" / Padding(12),
839841
)
840842
),

tests/test_paradox.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,71 @@ async def test_control_doors(mocker):
3939

4040
assert await alarm.control_door("Door 1", "unlock")
4141
alarm.panel.control_doors.assert_called_once_with([1], "unlock")
42+
43+
44+
def _add_module_pgm(alarm, module_address, pgm_index):
45+
key = f"module{module_address}_pgm{pgm_index}"
46+
alarm.storage.get_container("module_pgm")[key] = {
47+
"id": pgm_index,
48+
"key": key,
49+
"label": f"Module {module_address} PGM {pgm_index}",
50+
"module_address": module_address,
51+
"pgm_index": pgm_index,
52+
}
53+
alarm.storage.update_container_object("module_pgm", key, {"on": False})
54+
return key
55+
56+
57+
@pytest.mark.asyncio
58+
async def test_control_output_regular_pgm_routes_to_control_outputs(mocker):
59+
"""Regular PGM uses control_outputs, not control_module_pgm_outputs."""
60+
alarm = Paradox()
61+
alarm.panel = mocker.Mock(spec=Panel)
62+
alarm.panel.control_outputs = AsyncMock(return_value=True)
63+
alarm.panel.control_module_pgm_outputs = AsyncMock(return_value=True)
64+
65+
alarm.storage.get_container("pgm").deep_merge({1: {"id": 1, "key": "PGM 1"}})
66+
67+
assert await alarm.control_output("PGM 1", "on")
68+
alarm.panel.control_outputs.assert_called_once()
69+
alarm.panel.control_module_pgm_outputs.assert_not_called()
70+
71+
72+
@pytest.mark.asyncio
73+
async def test_control_output_module_pgm_routes_to_control_module_pgm_outputs(mocker):
74+
"""Module PGM uses control_module_pgm_outputs with the correct address and index."""
75+
alarm = Paradox()
76+
alarm.panel = mocker.Mock(spec=Panel)
77+
alarm.panel.control_outputs = AsyncMock(return_value=True)
78+
alarm.panel.control_module_pgm_outputs = AsyncMock(return_value=True)
79+
80+
_add_module_pgm(alarm, module_address=4, pgm_index=2)
81+
82+
assert await alarm.control_output("module4_pgm2", "on_override")
83+
alarm.panel.control_module_pgm_outputs.assert_called_once_with(4, 2, "on_override")
84+
alarm.panel.control_outputs.assert_not_called()
85+
86+
87+
@pytest.mark.asyncio
88+
async def test_control_output_module_pgm_optimistic_state(mocker):
89+
"""Accepted on_override sets on=True; off_override sets on=False."""
90+
alarm = Paradox()
91+
alarm.panel = mocker.Mock(spec=Panel)
92+
alarm.panel.control_module_pgm_outputs = AsyncMock(return_value=True)
93+
94+
key = _add_module_pgm(alarm, module_address=4, pgm_index=1)
95+
96+
await alarm.control_output(key, "on_override")
97+
assert alarm.storage.get_container("module_pgm")[key]["on"] is True
98+
99+
await alarm.control_output(key, "off_override")
100+
assert alarm.storage.get_container("module_pgm")[key]["on"] is False
101+
102+
103+
@pytest.mark.asyncio
104+
async def test_control_output_no_match_returns_false(mocker):
105+
"""Returns False when the output key matches neither pgm nor module_pgm."""
106+
alarm = Paradox()
107+
alarm.panel = mocker.Mock(spec=Panel)
108+
109+
assert await alarm.control_output("nonexistent", "on") is False

0 commit comments

Comments
 (0)