Summary
Deep review of the merged PRT3 implementation (PR #596) against the official PRT3 ASCII Programming Guide (pages 12-21) identified two categories of suspected protocol-spec divergence that need verification against real-panel captures before they can be confidently fixed.
This issue captures the findings for tracking; it does not propose code changes yet. The cheapest disambiguator (one short capture from a live PRT3) is described in the Verification section at the bottom — once that lands, we'll know which interpretation is right and can act accordingly.
File:line refs were valid at commit 04ed539 on branch pr3_followups.
Issue 1 — Area Status (RA) byte order (suspected)
File: paradox/hardware/prt3/parser.py:308-329 (_parse_area_status)
What the spec says (page 13)
After the 5-char prefix RA{nnn}:
| 0-indexed position |
spec value |
meaning |
| 5 |
D/A/F/S/I |
arm state |
| 6 |
M / O |
zone in memory |
| 7 |
T / O |
trouble |
| 8 |
N / O |
not ready |
| 9 |
P / O |
in programming |
| 10 |
A / O |
in alarm |
| 11 |
S / O |
strobe |
PRT3_Implementation_Plan.md:96 documents the same order: D/A/F/S/I, M/O, T/O, N/O, P/O, A/O, S/O.
What the implementation reads
| position |
impl checks |
maps to |
matches spec? |
| 5 |
D/A/F/S/I |
arm_state |
✓ |
| 6 |
P |
in_programming |
✗ (spec: M) |
| 7 |
T |
trouble |
✓ |
| 8 |
N |
not_ready |
✓ |
| 9 |
A |
alarm |
✗ (spec: P) |
| 10 |
S |
strobe |
✗ (spec: A) |
| 11 |
M |
zone_in_memory |
✗ (spec: S) |
Why this is hard to falsify from the test suite
The fixtures (tests/hardware/prt3/fixtures.py) and the single-flag tests (tests/hardware/prt3/test_parser.py:237-309) were authored in the same commit as the parser by the same author, citing the same spec PDF. The fixtures encode the parser's interpretation rather than independently observed panel output. No real-panel RA captures exist in the repo.
Worst-case risk if the spec is right
A real panel that emits a spec-compliant RA frame for an alarm-active partition would be read as alarm=False, strobe=False, zone_in_memory=False, in_programming=False. That is: an active alarm is invisible to PAI (and therefore to MQTT, Home Assistant, SMS / Pushbullet / Signal notifications). This is a critical-safety failure mode, but only triggers when a partition is actually in alarm — which is why it may never have been observed in normal-operation testing.
Issue 2 — EVENT_MAP misalignment
File: paradox/hardware/prt3/event.py:39-247
Cross-referenced against the spec event table (pages 17-21). The list below splits findings by confidence:
High-confidence (spec table is unambiguous)
| G |
Spec meaning |
impl maps as |
impact |
| 23 |
Zone Bypassed |
bypass_cancelled |
Inverted. First bypass event clears the flag; spec defines no separate "bypass cancelled" code, so bypassed zones never get re-set to bypassed=True in MemoryStorage. |
| 33 |
Zone Tamper |
system.trouble (empty change) |
Zone-tamper events never set tamper=True on the zone object. |
| 34 |
Zone Tamper Restore |
system.trouble_restored (empty change) |
Same problem, restore direction. |
| 56 |
(not defined in spec) |
zone bypassed |
Phantom event — would inject bypassed=True if firmware ever emits G56. |
| 60, 61 |
(spec says "Future Use") |
zone low_battery / zone supervision_trouble |
Same phantom-event risk. |
Medium-confidence (spec defines, mapping looks wrong)
| G |
Spec meaning |
impl maps as |
| 18 |
Disarm after alarm w/ Keyswitch |
alarm_cancelled |
| 20 |
Alarm Cancelled w/ User Code |
generic disarm |
| 21 |
Alarm Cancelled w/ Keyswitch |
zone bypassed |
| 29 |
Late to Disarm by User |
panic_alarm |
| 31 |
Duress Alarm by User |
tamper_alarm |
| 32 |
Zone Shutdown |
tamper_restored |
| 59 |
Module Removed from Combus |
zone closed |
| 62 |
Access Granted to User |
low_battery_restored |
| 63 |
Access Denied to User |
supervision_restored |
Granularity loss (collapsing subgrouped events)
| G |
Spec |
impl |
| 36 |
"Trouble Event" — 8 N-subnumbers (TLM / AC / Battery / Aux / Bell / Bell-absent / Clock / Global Fire) |
flat ac_failure |
| 38 |
"Module Trouble" — 9 N-subnumbers (Combus / Tamper / ROM / TLM / FTC / Printer / AC / Battery / Aux) |
flat battery_trouble |
| 30 |
"Special Alarm" — N0=Emergency / N1=Medical / N2=Fire panic / N3=Recent Closing / N4=Police / N5=Global Shutdown |
flat audible_alarm |
EVENT_MAP already supports the number_overrides pattern (see G065 at event.py:229); the same mechanism would resolve G36/G38/G30.
Completely missing (spec defines, impl omits)
G004, G005, G006, G007, G008, G019, G022, G028, G035, G037, G039, G040, G041–G044, G046, G047, G049–G055, G058.
Some of these are routine (G019/G020/G021 = alarm-cancelled, G041-G044 = zone battery/supervision trouble) and would be wanted for full status reporting.
Verification (cheapest path forward)
The right next step is not to change code — it's to capture a few seconds of real PRT3 output from a panel in a known state, place those captures in tests/hardware/prt3/captures/, and then decide.
RA byte-order test (2 minutes)
- Arm Area 1 to Stay mode. Leave other areas disarmed. No alarm, no trouble.
- Send
RA001\r to the PRT3 (any terminal: screen, picocom, etc.).
- Look at the 12-char response (after stripping
\r).
- If position 5 =
S and positions 6-11 are all O → consistent with both interpretations (need step 4).
- Trigger an alarm (open a non-delay zone while armed). Send
RA001\r again.
- If position 10 contains
A → spec is right, parser positions [6, 9, 10, 11] need to be fixed.
- If position 9 contains
A → impl is right, spec doc has a column-ordering quirk and we add a comment in parser.py citing the capture as authority.
EVENT_MAP test (5 minutes)
Enable LOGGING_DUMP_PACKETS=true and trigger the following sequence; copy the printed G... lines into a capture file:
- Bypass zone 1 via keypad → expect
G023N001A....
- Un-bypass zone 1 via keypad → check whether the panel emits a second G-line (spec doesn't define a separate cancel code, so observe what actually happens).
- Disarm via keypad → check whether it's G013 (Master), G014 (User), or G016/G017 (after-alarm variant).
- Trigger keypad panic (PE on emergency keys) → check G-code; spec says G030 with sub-N for type.
- Force an AC failure / battery test → check whether it's G036 (with sub-N) or G038 (with sub-N).
Capture file placement
Suggested: tests/hardware/prt3/captures/ (new directory, module-scoped — analogous to how tests/hardware/prt3/fixtures.py is module-scoped). Each capture file should include a brief header comment naming the panel model, firmware version, and the action being captured.
Followups context
PR #596 had a separate three-perspective review captured in PRT3_Followups.md; this issue is orthogonal to those items. None of the C1/C2/M3/M6/R1-R5 follow-ups touch the parser byte order or EVENT_MAP table.
The simple PRT3 follow-ups (M1, M2, M4, M5, plus minor cleanups) were addressed in commit 04ed539 on branch pr3_followups.
Suggested staging (only after verification)
If captures confirm the spec interpretation, the Architect's review proposed a 5-stage rollout to keep tests green at each step:
- Stage 0 — land the captures (no code change).
- Stage 1 — extract magic numbers in
parser.py into named constants (no semantic change).
- Stage 2 — flip ONE RA position (canary) + new spec-compliant fixture, mark old fixtures
@pytest.mark.legacy_ra_order.
- Stage 3 — flip remaining RA positions.
- Stage 4 / 5 — one G-per-commit EVENT_MAP fixes, then drop legacy fixtures.
A PRT3_STRICT_SPEC_MODE config flag is not recommended — single user, six-day-old PR, no persisted state depending on event taxonomy, simpler to coordinate one minor-version bump.
cc @NaanyaBiz — would you be able to grab the captures above when you next have hands on your panel? That's the gating step before any code change here.
Summary
Deep review of the merged PRT3 implementation (PR #596) against the official PRT3 ASCII Programming Guide (pages 12-21) identified two categories of suspected protocol-spec divergence that need verification against real-panel captures before they can be confidently fixed.
This issue captures the findings for tracking; it does not propose code changes yet. The cheapest disambiguator (one short capture from a live PRT3) is described in the Verification section at the bottom — once that lands, we'll know which interpretation is right and can act accordingly.
Issue 1 — Area Status (RA) byte order (suspected)
File:
paradox/hardware/prt3/parser.py:308-329(_parse_area_status)What the spec says (page 13)
After the 5-char prefix
RA{nnn}:PRT3_Implementation_Plan.md:96documents the same order:D/A/F/S/I, M/O, T/O, N/O, P/O, A/O, S/O.What the implementation reads
arm_statePin_programmingTtroubleNnot_readyAalarmSstrobeMzone_in_memoryWhy this is hard to falsify from the test suite
The fixtures (
tests/hardware/prt3/fixtures.py) and the single-flag tests (tests/hardware/prt3/test_parser.py:237-309) were authored in the same commit as the parser by the same author, citing the same spec PDF. The fixtures encode the parser's interpretation rather than independently observed panel output. No real-panel RA captures exist in the repo.Worst-case risk if the spec is right
A real panel that emits a spec-compliant RA frame for an alarm-active partition would be read as
alarm=False, strobe=False, zone_in_memory=False, in_programming=False. That is: an active alarm is invisible to PAI (and therefore to MQTT, Home Assistant, SMS / Pushbullet / Signal notifications). This is a critical-safety failure mode, but only triggers when a partition is actually in alarm — which is why it may never have been observed in normal-operation testing.Issue 2 — EVENT_MAP misalignment
File:
paradox/hardware/prt3/event.py:39-247Cross-referenced against the spec event table (pages 17-21). The list below splits findings by confidence:
High-confidence (spec table is unambiguous)
bypass_cancelledbypassed=TrueinMemoryStorage.system.trouble(emptychange)tamper=Trueon the zone object.system.trouble_restored(emptychange)zone bypassedbypassed=Trueif firmware ever emits G56.zone low_battery/zone supervision_troubleMedium-confidence (spec defines, mapping looks wrong)
alarm_cancelleddisarmzone bypassedpanic_alarmtamper_alarmtamper_restoredzone closedlow_battery_restoredsupervision_restoredGranularity loss (collapsing subgrouped events)
ac_failurebattery_troubleaudible_alarmEVENT_MAP already supports the
number_overridespattern (see G065 atevent.py:229); the same mechanism would resolve G36/G38/G30.Completely missing (spec defines, impl omits)
G004, G005, G006, G007, G008, G019, G022, G028, G035, G037, G039, G040, G041–G044, G046, G047, G049–G055, G058.
Some of these are routine (G019/G020/G021 = alarm-cancelled, G041-G044 = zone battery/supervision trouble) and would be wanted for full status reporting.
Verification (cheapest path forward)
The right next step is not to change code — it's to capture a few seconds of real PRT3 output from a panel in a known state, place those captures in
tests/hardware/prt3/captures/, and then decide.RA byte-order test (2 minutes)
RA001\rto the PRT3 (any terminal:screen,picocom, etc.).\r).Sand positions 6-11 are allO→ consistent with both interpretations (need step 4).RA001\ragain.A→ spec is right, parser positions [6, 9, 10, 11] need to be fixed.A→ impl is right, spec doc has a column-ordering quirk and we add a comment inparser.pyciting the capture as authority.EVENT_MAP test (5 minutes)
Enable
LOGGING_DUMP_PACKETS=trueand trigger the following sequence; copy the printedG...lines into a capture file:G023N001A....Capture file placement
Suggested:
tests/hardware/prt3/captures/(new directory, module-scoped — analogous to howtests/hardware/prt3/fixtures.pyis module-scoped). Each capture file should include a brief header comment naming the panel model, firmware version, and the action being captured.Followups context
PR #596 had a separate three-perspective review captured in
PRT3_Followups.md; this issue is orthogonal to those items. None of the C1/C2/M3/M6/R1-R5 follow-ups touch the parser byte order or EVENT_MAP table.The simple PRT3 follow-ups (M1, M2, M4, M5, plus minor cleanups) were addressed in commit
04ed539on branchpr3_followups.Suggested staging (only after verification)
If captures confirm the spec interpretation, the Architect's review proposed a 5-stage rollout to keep tests green at each step:
parser.pyinto named constants (no semantic change).@pytest.mark.legacy_ra_order.A
PRT3_STRICT_SPEC_MODEconfig flag is not recommended — single user, six-day-old PR, no persisted state depending on event taxonomy, simpler to coordinate one minor-version bump.cc @NaanyaBiz — would you be able to grab the captures above when you next have hands on your panel? That's the gating step before any code change here.