Skip to content

Commit ffdad6e

Browse files
authored
Merge pull request #599 from ParadoxAlarmInterface/pr3_followups
fix: address simple PRT3 review follow-ups
2 parents 1810333 + e998b11 commit ffdad6e

8 files changed

Lines changed: 433 additions & 197 deletions

File tree

paradox/connections/prt3/protocol.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def data_received(self, data: bytes):
5959
if cfg.LOGGING_DUMP_PACKETS:
6060
logger.debug("PRT3 <- %s", binascii.hexlify(line_with_cr))
6161

62-
if line.strip(): # skip empty / whitespace-only lines
62+
if line.strip(): # skip empty / whitespace-only lines
6363
self.handler.on_message(line_with_cr)
6464

6565
def send_message(self, message: bytes):
@@ -72,6 +72,11 @@ def send_message(self, message: bytes):
7272
self.check_active()
7373

7474
if cfg.LOGGING_DUMP_PACKETS:
75-
logger.debug("PRT3 -> %s", binascii.hexlify(message))
75+
# AA (arm with code) and AD (disarm) embed the user code in the
76+
# payload; show only the command prefix to keep it out of logs.
77+
if message[:2] in (b"AA", b"AD"):
78+
logger.debug("PRT3 -> %s<redacted>", binascii.hexlify(message[:5]))
79+
else:
80+
logger.debug("PRT3 -> %s", binascii.hexlify(message))
7681

7782
self.transport.write(message)

paradox/hardware/prt3/panel.py

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
from paradox.config import config as cfg
4242
from paradox.hardware.panel import Panel
4343
from paradox.hardware.prt3 import adapter, encoder
44-
from paradox.hardware.prt3.event import EVENT_MAP, PRT3Event
4544
from paradox.hardware.prt3.parser import (
4645
PRT3AreaStatus,
4746
PRT3BufferFull,
@@ -62,19 +61,19 @@
6261
# Maps PAI command string → (encoder_fn, arm_mode_char)
6362
# All arm variants that don't require a user code use quick-arm.
6463
_QUICK_ARM_MODES = {
65-
"arm": "A", # default arm → away
66-
"arm_away": "A",
67-
"arm_stay": "S",
68-
"arm_force": "F",
64+
"arm": "A", # default arm → away
65+
"arm_away": "A",
66+
"arm_stay": "S",
67+
"arm_force": "F",
6968
"arm_instant": "I",
70-
"arm_sleep": "I", # instant arm (no entry delay) — PRT3 closest to HA arm_night
69+
"arm_sleep": "I", # instant arm (no entry delay) — PRT3 closest to HA arm_night
7170
}
7271

7372
# Panic type → encoder function
7473
_PANIC_ENCODERS = {
7574
"emergency": encoder.encode_panic_emergency,
76-
"medical": encoder.encode_panic_medical,
77-
"fire": encoder.encode_panic_fire,
75+
"medical": encoder.encode_panic_medical,
76+
"fire": encoder.encode_panic_fire,
7877
}
7978

8079

@@ -109,6 +108,24 @@ def __init__(self, core):
109108
cfg.PRT3_MAX_ZONES * cfg.IO_TIMEOUT,
110109
cfg.KEEP_ALIVE_INTERVAL,
111110
)
111+
# Labels are loaded serially at startup; with PRT3_MAX_USERS=999 and
112+
# IO_TIMEOUT=0.5 the worst-case (all timeouts) is ~8 min just for users.
113+
# Warn when the combined worst-case load time exceeds 2 min so it
114+
# doesn't fire on the default 8+96+32 config.
115+
label_load_seconds = (
116+
cfg.PRT3_MAX_AREAS + cfg.PRT3_MAX_ZONES + cfg.PRT3_MAX_USERS
117+
) * cfg.IO_TIMEOUT
118+
if label_load_seconds > 120:
119+
logger.warning(
120+
"PRT3: startup label load may take up to %.0f s "
121+
"(%d areas + %d zones + %d users at %.1f s timeout) — "
122+
"consider reducing PRT3_MAX_USERS / PRT3_MAX_ZONES",
123+
label_load_seconds,
124+
cfg.PRT3_MAX_AREAS,
125+
cfg.PRT3_MAX_ZONES,
126+
cfg.PRT3_MAX_USERS,
127+
cfg.IO_TIMEOUT,
128+
)
112129

113130
# ------------------------------------------------------------------
114131
# Message parsing
@@ -160,7 +177,10 @@ async def _prt3_send_wait(
160177
timed out or hit buffer-full.
161178
"""
162179
async with self.core.request_lock:
163-
buffer_full_or_match = lambda m: predicate(m) or isinstance(m, PRT3BufferFull)
180+
181+
def buffer_full_or_match(m):
182+
return predicate(m) or isinstance(m, PRT3BufferFull)
183+
164184
for attempt in range(1, retries + 1):
165185
self.core.connection.write(command_bytes)
166186
try:
@@ -170,15 +190,19 @@ async def _prt3_send_wait(
170190
if isinstance(result, PRT3BufferFull):
171191
logger.warning(
172192
"PRT3: buffer full on attempt %d/%d, retrying: %s",
173-
attempt, retries, command_bytes[:5], # [:5] excludes user code
193+
attempt,
194+
retries,
195+
command_bytes[:5], # [:5] excludes user code
174196
)
175197
continue
176198
return result
177199
except asyncio.TimeoutError:
178200
if attempt < retries:
179201
logger.warning(
180202
"PRT3: timeout on attempt %d/%d, retrying: %s",
181-
attempt, retries, command_bytes[:5], # [:5] excludes user code
203+
attempt,
204+
retries,
205+
command_bytes[:5], # [:5] excludes user code
182206
)
183207
return None
184208

@@ -210,10 +234,18 @@ async def initialize_communication(self, password) -> bool:
210234
"""
211235
from paradox.hardware.prt3.parser import PRT3SystemEvent
212236

213-
_any_prt3_msg = lambda m: isinstance(
214-
m, (PRT3CommStatus, PRT3AreaStatus, PRT3ZoneStatus, PRT3LabelReply,
215-
PRT3CommandEcho, PRT3SystemEvent)
216-
)
237+
def _any_prt3_msg(m):
238+
return isinstance(
239+
m,
240+
(
241+
PRT3CommStatus,
242+
PRT3AreaStatus,
243+
PRT3ZoneStatus,
244+
PRT3LabelReply,
245+
PRT3CommandEcho,
246+
PRT3SystemEvent,
247+
),
248+
)
217249

218250
logger.info("PRT3: checking serial link liveness")
219251

@@ -242,11 +274,14 @@ async def initialize_communication(self, password) -> bool:
242274
if isinstance(msg, PRT3CommStatus) and not msg.ok:
243275
logger.error("PRT3: panel communication failure (COMM&fail)")
244276
return False
245-
logger.info("PRT3: serial link live (probe response: %s)", type(msg).__name__)
277+
logger.info(
278+
"PRT3: serial link live (probe response: %s)", type(msg).__name__
279+
)
246280
return True
247281
except asyncio.TimeoutError:
248282
logger.error(
249-
"PRT3: serial link unresponsive after %.0fs probe", cfg.PRT3_COMM_TIMEOUT
283+
"PRT3: serial link unresponsive after %.0fs probe",
284+
cfg.PRT3_COMM_TIMEOUT,
250285
)
251286
return False
252287

@@ -273,7 +308,11 @@ async def _load_label_range(
273308
msg = await self._prt3_send_wait(
274309
cmd,
275310
lambda m, ec=expected_cmd, et=element_type, idx=i: (
276-
(isinstance(m, PRT3LabelReply) and m.element_type == et and m.index == idx)
311+
(
312+
isinstance(m, PRT3LabelReply)
313+
and m.element_type == et
314+
and m.index == idx
315+
)
277316
or (isinstance(m, PRT3CommandEcho) and m.cmd == ec)
278317
),
279318
)
@@ -297,9 +336,15 @@ async def load_labels(self) -> dict:
297336
"""
298337
logger.info("PRT3: loading labels")
299338
replies = (
300-
await self._load_label_range("area", cfg.PRT3_MAX_AREAS, encoder.encode_area_label_request, "AL")
301-
+ await self._load_label_range("zone", cfg.PRT3_MAX_ZONES, encoder.encode_zone_label_request, "ZL")
302-
+ await self._load_label_range("user", cfg.PRT3_MAX_USERS, encoder.encode_user_label_request, "UL")
339+
await self._load_label_range(
340+
"area", cfg.PRT3_MAX_AREAS, encoder.encode_area_label_request, "AL"
341+
)
342+
+ await self._load_label_range(
343+
"zone", cfg.PRT3_MAX_ZONES, encoder.encode_zone_label_request, "ZL"
344+
)
345+
+ await self._load_label_range(
346+
"user", cfg.PRT3_MAX_USERS, encoder.encode_user_label_request, "UL"
347+
)
303348
)
304349
labels = adapter.labels_dict_from_replies(replies)
305350
logger.info(
@@ -407,7 +452,10 @@ def _build_partition_cmd(
407452
mode = _QUICK_ARM_MODES[command]
408453
if user_code:
409454
try:
410-
return encoder.encode_arm(partition, mode, user_code), f"AA{partition:03d}"
455+
return (
456+
encoder.encode_arm(partition, mode, user_code),
457+
f"AA{partition:03d}",
458+
)
411459
except ValueError as exc:
412460
logger.error("PRT3: invalid PRT3_USER_CODE for arm: %s", exc)
413461
return None
@@ -439,7 +487,7 @@ async def control_partitions(self, partitions: list, command: str) -> bool:
439487
lambda m, ec=expected_echo: (
440488
isinstance(m, PRT3CommandEcho) and m.cmd == ec
441489
),
442-
retries=2,
490+
retries=1, # non-idempotent: retry can double-arm if echo was lost
443491
)
444492
if msg is None:
445493
logger.warning("PRT3: timeout on %s partition %d", command, partition)
@@ -448,7 +496,9 @@ async def control_partitions(self, partitions: list, command: str) -> bool:
448496
accepted = True
449497
else:
450498
logger.warning(
451-
"PRT3: %s partition %d rejected by panel (&fail)", command, partition
499+
"PRT3: %s partition %d rejected by panel (&fail)",
500+
command,
501+
partition,
452502
)
453503

454504
return accepted
@@ -498,10 +548,12 @@ async def send_panic(self, partitions: list, panic_type: str, _code) -> bool:
498548
lambda m, ec=expected_echo: (
499549
isinstance(m, PRT3CommandEcho) and m.cmd == ec
500550
),
501-
retries=2,
551+
retries=1, # non-idempotent: retry can re-trigger panic if echo was lost
502552
)
503553
if msg is None:
504-
logger.warning("PRT3: timeout on %s panic area %d", panic_type, partition)
554+
logger.warning(
555+
"PRT3: timeout on %s panic area %d", panic_type, partition
556+
)
505557
continue
506558
if not msg.ok:
507559
logger.warning(

0 commit comments

Comments
 (0)