Skip to content

feat: add PRT3 ASCII serial connection type#596

Open
NaanyaBiz wants to merge 20 commits intoParadoxAlarmInterface:devfrom
NaanyaBiz:feature/prt3-connection
Open

feat: add PRT3 ASCII serial connection type#596
NaanyaBiz wants to merge 20 commits intoParadoxAlarmInterface:devfrom
NaanyaBiz:feature/prt3-connection

Conversation

@NaanyaBiz
Copy link
Copy Markdown

Summary

This PR adds support for the Paradox PRT3 printer module as a first-class
connection type alongside the existing Serial and IP backends. The PRT3
exposes a documented ASCII serial protocol over a standard DB9/USB-serial cable,
providing a practical integration path for panels where the native binary protocol
and IP module are no longer accessible.

Motivation

Two trends have progressively closed off the existing integration paths for Paradox
panels:

  1. Native serial encryption — newer EVO/Spectra firmware encrypts the binary
    serial link, making reverse-engineered integrations unreliable or broken outright
    on updated panels.

  2. IP150 lockdown — recent IP150 firmware versions restrict connections from
    third-party software, breaking IP-based integrations on many panels.

The PRT3 module is unaffected by both. Paradox publishes and maintains its ASCII
protocol; it has been stable across firmware generations and works over commodity
USB-to-serial hardware. Making PRT3 a supported PAI backend preserves integration
capability for a meaningful portion of the Paradox install base.


Architecture

PRT3 is implemented as a peer connection type, not a wrapper around the existing
native serial path.

  • PRT3SerialConnection extends SerialCommunication, overriding only the protocol
    factory to produce a line-oriented ASCII framer instead of binary.
  • PRT3Panel extends the same Panel base class as EVO/Spectra panels, implementing
    all required methods via ASCII commands (RA/RZ for status, AL/ZL/UL for
    labels, AA/AQ/AD for arm/disarm, UK for utility keys, PE/PM/PF for
    panic).
  • paradox.py selects PRT3 in the existing if/elif/else connection type chain.
  • State flows through the same MemoryStorage → MQTT pipeline as other backends.

The only special-casing outside the prt3/ module is:

  • 3 connection-selection branches (peer with IP/Serial)
  • 3 protocol-gap guards (sync_time, _clean_session, control_utility_key guard)
  • 1 event handler registration (PRT3 dispatches dataclass events, not binary Containers)
  • 1 optional MQTT feature (utility key button discovery, gated on PRT3_UTILITY_KEYS)

Total: 9 conditionals, all justified by genuine protocol differences.


What's included

New files

Path Purpose
paradox/connections/prt3/connection.py Serial connection; ASCII line framer
paradox/connections/prt3/protocol.py asyncio protocol; splits on \r
paradox/hardware/prt3/parser.py Parses all PRT3 ASCII lines into dataclasses
paradox/hardware/prt3/encoder.py Builds PRT3 command strings
paradox/hardware/prt3/panel.py Full Panel implementation
paradox/hardware/prt3/event.py Maps PRT3 G-group events → PAI Event objects
paradox/hardware/prt3/adapter.py Normalises PRT3 status replies into PAI storage model
paradox/hardware/prt3/property.py Property key definitions
paradox/hardware/prt3/runtime.py (reserved for future PRT3-specific runtime extensions)
paradox/interfaces/mqtt/entities/button.py UtilityKeyButton HA discovery entity
docs/prt3-usage.md Setup, configuration, and limitations guide

Modified files

Path Change
paradox/paradox.py PRT3 connect path, event dispatch, utility key control, protocol guards
paradox/config.py PRT3_* config keys, MQTT_UTILITY_KEY_TOPIC
paradox/interfaces/mqtt/entities/factory.py make_utility_key_button()
paradox/interfaces/mqtt/homeassistant.py _publish_utility_key_configs()
paradox/interfaces/mqtt/basic.py Utility key MQTT subscription and handler
paradox/interfaces/mqtt/core.py Bug fix: fire on_connect for late-registering interfaces
config/pai.conf.example PRT3 section with annotated options

Tests

File Coverage
tests/hardware/prt3/test_parser.py Full parser round-trip, edge cases
tests/hardware/prt3/test_encoder.py All command types, boundary values
tests/hardware/prt3/test_adapter.py Area/zone status normalisation
tests/hardware/prt3/test_command_dispatch.py Command send/echo/fail/timeout paths
tests/hardware/prt3/test_panel.py initialize_communication, load_labels, request_status
tests/hardware/prt3/test_mqtt_integration.py HA discovery, utility key buttons, event passthrough
tests/hardware/prt3/fixtures.py Verified ASCII fixture lines for all message types
tests/connection/prt3/test_protocol.py Line framing and protocol parsing

Configuration

CONNECTION_TYPE = 'PRT3'
PRT3_SERIAL_PORT = '/dev/ttyUSB0'
PRT3_SERIAL_BAUD = 9600
PRT3_USER_CODE = '1234'       # Required for disarm; leave empty for quick-arm only
PRT3_MAX_AREAS = 2
PRT3_MAX_ZONES = 32
PRT3_UTILITY_KEYS = {
    1: 'Lock Front Gate',
    2: 'Garden Lights',
}

Full options documented in config/pai.conf.example and docs/prt3-usage.md.


Known limitations (v1)

  • Zone bypass/force not supported (no PRT3 command exists)
  • Virtual PGM outputs parsed but not controllable
  • No SetTimeDate command — SYNC_TIME is a no-op
  • Area count must be set in config (panel does not report enrolled count)
  • Status established by polling on connect; brief gap on reconnect is possible
  • Utility keys are not idempotent — PAI deliberately sends no retries

Testing

pytest tests/hardware/prt3/ tests/connection/prt3/ -v

All tests use synchronous or asyncio-mocked transports; no hardware required.

Naanya Biz and others added 13 commits March 29, 2026 18:26
Describes why PRT3 is a separate connection type from native serial,
where the transport/protocol/adapter layers live, what can be reused
from existing PAI, the v1 scope, and documented limitations.
Maps the approved architecture onto the actual PAI codebase: exact
insertion points in config.py and paradox.py, full spec for every new
file/class, runtime path analysis, test patterns for each layer, phased
delivery plan, and risk register.
Add the minimum non-invasive scaffolding for the PRT3 connection type:

- paradox/config.py: add "PRT3" to CONNECTION_TYPE allowed list; add
  PRT3_SERIAL_PORT, PRT3_SERIAL_BAUD, PRT3_MAX_AREAS, PRT3_MAX_ZONES,
  PRT3_MAX_USERS config keys.

- paradox/paradox.py: add PRT3 branch in connection property
  (instantiates PRT3SerialConnection); add guard in connect() that
  returns False with an error log — binary panel detection does not
  apply to PRT3.

- paradox/connections/prt3/: PRT3Protocol (ASCII line framer skeleton)
  and PRT3SerialConnection (only overrides make_protocol()).

- paradox/hardware/prt3/: PRT3Panel, PRT3Paradox, parser dataclasses,
  encoder stubs, PRT3Event, property_map re-export from
  spectra_magellan.  All protocol logic raises NotImplementedError
  with Phase 2/3 TODO labels.

- tests/connection/prt3/, tests/hardware/prt3/: import smoke tests
  and NotImplementedError assertions; 869 existing tests unaffected.

- docs/prt3-architecture.md: branch status note added.

No existing Serial or IP150 behaviour is changed.
Implements the complete PRT3 ASCII protocol layer:

parser.py: parse_line() converts raw \r-stripped ASCII lines into typed
dataclasses (PRT3CommStatus, PRT3BufferFull, PRT3CommandEcho,
PRT3AreaStatus, PRT3ZoneStatus, PRT3LabelReply, PRT3SystemEvent,
PRT3PgmEvent). Malformed lines log WARNING and return None; no exceptions
raised to caller.

encoder.py: pure functions for all v1 outbound commands (RA/RZ/AL/ZL/UL
status/label requests; AA/AQ/AD arm/quick-arm/disarm; PE/PM/PF panic;
UK utility key). Each returns bytes including trailing \r. Input
validated; out-of-range args raise ValueError. No undocumented commands.

tests/hardware/prt3/fixtures.py: position-verified PRT3 wire-format
fixture strings for all message types, plus ALL_FIXTURES collection for
replay tests.

tests/hardware/prt3/test_encoder.py: 339 tests covering exact output
format, boundary values (area 1/8, zone 1/192, user 1/999, key 1/251),
all arm modes, code length validation, error cases, echo prefix table,
and cross-cutting bytes/ASCII/\r assertions.

tests/hardware/prt3/test_parser.py: full fixture-driven parser coverage
including all flag combinations, boundary numbers, disambiguation of
COMM status vs command echoes, failed info commands, malformed/unknown
lines, and all-fixtures smoke test.

1194 tests pass; 0 regressions.
adapter.py: pure normalization boundary — arm_state enum to PAI boolean
partition flags, zone open_state to PAI zone flags, label reply to PAI
labels dict, build_flat_status for convert_raw_status compatibility.

panel.py: Phase 2 — parse_message delegates to parse_line, initialize_
communication awaits COMM&ok, load_labels polls AL/ZL/UL sequentially,
request_status polls RA/RZ and returns flat status dict (single virtual
address 0 covers all configured areas+zones), control_partitions maps
arm/disarm commands to AA/AQ/AD (with PRT3_USER_CODE fallback to quick-
arm), send_panic maps to PE/PM/PF, _prt3_send_wait helper for typed
request/reply via wait_for_message.

event.py: EVENT_MAP for G-groups 0-66 (zone, arm/disarm, bypass, alarm,
panic, trouble, power-up, utility key, status broadcasts). PRT3Event.
from_prt3 constructs PAI Event without LiveEvent binary assertions.

protocol.py: PRT3Protocol framer complete. data_received buffers bytes
and emits complete cr-terminated lines. send_message writes raw bytes.

Compat fixes: EventMessageHandler/ErrorMessageHandler can_handle guards
against non-Container types. HandlerRegistry no-handler log guarded.

config.py: PRT3_USER_CODE and PRT3_COMM_TIMEOUT config keys.

1260 tests pass.
- paradox.py: implement PRT3 connect() path — ASCII COMM&ok handshake,
  synthesised DetectedPanel for stable HA entity anchoring, event handler
  registration, control_utility_key() dispatcher
- paradox.py: guard sync_time() and _clean_session() — PRT3 has no
  SetTimeDate or CloseConnection frame
- hardware/prt3/panel.py: full panel implementation — initialize_communication,
  load_labels (AL/ZL/UL), request_status (RA/RZ), control_partitions
  (AA/AQ/AD), send_utility_key (UK), send_panic (PE/PM/PF); retries=1 on
  utility key (commands are not idempotent)
- hardware/prt3/event.py: complete event map for G000-G066; G065 per-N
  dispatch (N001=exit_delay, N002=entry_delay) so HA shows "arming" state
  during countdown without flickering; arm/disarm events clear exit_delay
- hardware/prt3/parser.py: parser fixes and PGM line support
- config.py: add PRT3_UTILITY_KEYS and MQTT_UTILITY_KEY_TOPIC
- config/pai.conf.example: PRT3 section with annotated options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- entities/button.py: new UtilityKeyButton entity — command_topic wired to
  paradox/{control}/{utility_key}/{n}, HA discovery config with payload_press
- entities/factory.py: make_utility_key_button() factory method
- homeassistant.py: _publish_utility_key_configs() publishes one button
  discovery config per PRT3_UTILITY_KEYS entry on status_update
- basic.py: subscribe paradox/control/utility_key/+ when CONNECTION_TYPE=PRT3;
  _mqtt_handle_utility_key() dispatches to control_utility_key()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When an interface registers with MQTTConnection after the broker connection
is already established, on_connect was never called, leaving control
subscriptions and connected_future in an uninitialised state.  Fire
on_connect immediately in register() when already connected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_command_dispatch.py: arm/disarm/quick-arm/panic/utility-key command
  encoding and echo matching; failure and timeout paths
- test_panel.py: initialize_communication, load_labels, request_status;
  COMM&ok / COMM&fail paths
- test_mqtt_integration.py: panel_detected on connect; UtilityKeyButton
  serialize and topic construction; factory wiring; _publish_utility_key_configs
  publishes one config per key; PRT3Event timestamp/subtype/props; G065 per-N
  exit/entry delay dispatch; arm/disarm clears exit_delay
- fixtures.py: add G065 exit/entry delay event fixtures, PGM fixtures
- test_parser.py: updated for PGM line and edge cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers hardware wiring, baud configuration, user code setup, utility keys,
HA entity types, arming state mapping, and v1 limitations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test expected send_utility_key to retry on timeout and succeed on the
second attempt.  Retries were removed to prevent gate/latch double-triggering
(utility key commands are not idempotent).  Updated test asserts False return
and exactly one write on timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- protocol.py: guard against buffer overflow (>512 bytes)
- panel.py: send_panic() accepts list[int] to match EVO/SM signature;
  add arm_sleep alias (instant arm); warn when zone-poll timeout exceeds
  half of KEEP_ALIVE_INTERVAL
- runtime.py: remove TODO-stub file (logic lives in panel.py / paradox.py)
- config.py: lower PRT3_MAX_USERS default to 32; add security note on
  PRT3_USER_CODE storage
- mqtt/core.py: replay correct 5-arg on_connect signature to late
  registrars
- mqtt/abstract_entity.py: fix format() → format_map() with correct dict
  literal (was a set literal — would have raised TypeError at runtime)
- mqtt/button.py: same format_map fix
- paradox.py: re-raise CancelledError from control_utility_key instead
  of swallowing it
- tests: update panic tests to pass [int] list; update CancelledError
  test to assert re-raise

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
prt3-architecture.md: remove stale "not yet functional" status section,
add adapter.py to layer layout, remove deleted runtime.py, expand
wiring section to reflect actual scope (MQTT utility key integration,
full PRT3_* config key list).

prt3-implementation-plan.md: removed — planning artifact; the
PRT3Paradox subclass design it describes was superseded by the guard
approach that was actually built.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 17, 2026 03:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new first-class PRT3 ASCII serial backend (Paradox PRT3 printer module) alongside existing Serial/IP paths, including parsing/encoding, panel implementation, event mapping, and MQTT/HA utility-key button support.

Changes:

  • Add PRT3 connection/protocol framing and a full PRT3Panel implementation (parser, encoder, adapter, event mapping).
  • Integrate PRT3 into runtime selection (paradox.py) and add protocol-gap guards plus PRT3 event dispatch.
  • Extend MQTT + Home Assistant discovery with Utility Key button entities and handling; add comprehensive PRT3-focused tests and docs.

Reviewed changes

Copilot reviewed 31 out of 33 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
paradox/paradox.py Adds PRT3 connection selection, connect path, utility-key control API, and PRT3 event dispatch integration.
paradox/config.py Adds PRT3_* config keys and MQTT_UTILITY_KEY_TOPIC.
paradox/connections/prt3/connection.py Introduces PRT3SerialConnection using ASCII protocol framing.
paradox/connections/prt3/protocol.py Implements CR-delimited ASCII line framer for PRT3.
paradox/connections/prt3/__init__.py Declares PRT3 connection package.
paradox/hardware/prt3/parser.py Parses PRT3 ASCII lines into typed dataclasses.
paradox/hardware/prt3/encoder.py Encodes PRT3 ASCII commands (status/labels/arm/disarm/panic/utility key).
paradox/hardware/prt3/adapter.py Normalizes PRT3 status/labels into PAI’s storage/status formats.
paradox/hardware/prt3/event.py Maps PRT3 G-group events into PAI Event objects.
paradox/hardware/prt3/panel.py Implements Panel API over PRT3 polling + control + async events.
paradox/hardware/prt3/property.py Re-exports existing property map for consistency with other panels.
paradox/hardware/prt3/__init__.py Declares PRT3 hardware package.
paradox/lib/async_message_manager.py Guards event/error handlers so non-binary (PRT3) messages don’t assert/crash.
paradox/lib/handlers.py Makes “No handler” logging robust for non-Container message types.
paradox/interfaces/mqtt/core.py Replays on_connect to late-registered interfaces (fix for missed subscriptions).
paradox/interfaces/mqtt/basic.py Subscribes + handles PRT3 utility-key MQTT commands.
paradox/interfaces/mqtt/homeassistant.py Publishes HA discovery configs for PRT3 utility-key buttons.
paradox/interfaces/mqtt/entities/factory.py Adds factory method to build utility-key button entities.
paradox/interfaces/mqtt/entities/button.py Adds UtilityKeyButton HA discovery entity for PRT3 utility keys.
paradox/interfaces/mqtt/entities/abstract_entity.py Fixes entity prefix formatting (format_map) used in HA discovery serialization.
tests/connection/prt3/test_protocol.py Tests PRT3 protocol framing, buffering, and send behavior.
tests/connection/prt3/__init__.py Declares test package for PRT3 connection.
tests/hardware/prt3/fixtures.py Adds verified PRT3 ASCII fixture lines for deterministic tests.
tests/hardware/prt3/test_parser.py Extensive coverage of PRT3 parser behavior and edge cases.
tests/hardware/prt3/test_encoder.py Validates command encoding, echo prefixes, ASCII, and boundaries.
tests/hardware/prt3/test_adapter.py Tests normalization into PAI storage/status formats.
tests/hardware/prt3/test_panel.py Tests request/reply cycles and retry behavior for panel methods.
tests/hardware/prt3/test_command_dispatch.py Tests Paradox control dispatch for partitions and utility keys.
tests/hardware/prt3/test_mqtt_integration.py Tests panel_detected emission, HA discovery JSON, and event passthrough.
tests/hardware/prt3/__init__.py Declares test package for PRT3 hardware.
docs/prt3-usage.md Documents setup, configuration, HA behavior, and limitations.
docs/prt3-architecture.md Documents PRT3 layering, runtime wiring points, and v1 scope.
config/pai.conf.example Adds example PRT3 configuration and MQTT utility-key topic.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread paradox/interfaces/mqtt/homeassistant.py
Comment thread paradox/hardware/prt3/panel.py
Comment thread paradox/config.py Outdated
Comment thread paradox/paradox.py Outdated
Comment thread paradox/paradox.py
Comment thread paradox/interfaces/mqtt/homeassistant.py Outdated
Naanya Biz and others added 3 commits April 17, 2026 14:14
event.py: replace dict() constructor calls with {} literals (43 instances)
button.py: same (1 instance)
parser.py: reword PRT3PgmEvent.on inline comment to avoid false
  "commented-out code" flag; refactor parse_line to pre-compute
  has_digit_index — eliminates repeated and-chains and 3 nesting levels,
  reducing cognitive complexity from 23 to 12
panel.py: extract _load_label_range() from load_labels (complexity 27→8),
  extract _poll_area_statuses() and _poll_zone_statuses() from
  request_status (complexity 18→5), extract _build_partition_cmd() from
  control_partitions (complexity 17→9)
paradox.py: extract _prt3_connect() from connect() (complexity 20→13)
test_protocol.py: add explanatory comments to empty interface stubs;
  rename unused 'handler' variable to '_'
test_adapter.py: rename unused loop variables to '_'
test_encoder.py: add NOSONAR to intentional wrong-type test argument

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
homeassistant.py: gate utility-key discovery publish on
  CONNECTION_TYPE == 'PRT3' to prevent non-functional HA buttons on
  non-PRT3 connections; add 1..251 range check with warning in
  _publish_utility_key_configs()
panel.py: redact user code from retry-timeout log — log only the
  5-char command prefix instead of full command bytes
config.py: allow PRT3_MAX_ZONES and PRT3_MAX_USERS to be set to 0 to
  disable zone polling / user label loading respectively
paradox.py: fix misleading ConnectionError message on link-unresponsive
  failure; catch ValueError/TypeError in control_utility_key() for
  out-of-range key numbers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
panel.py: remove vestigial 'timeout' parameter from _prt3_send_wait —
  no caller passed an explicit value; cfg.IO_TIMEOUT is used directly
parser.py: extract _parse_info_reply() from parse_line, reducing
  cognitive complexity from 22 to 12 (limit: 15)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three observed bugs caused HA to show wrong partition state:

1. Stuck "arming" after a quick arm+disarm cycle. G014 (Disarm with
   User Code) can fire with area=0 (global) on at least some firmware;
   the previous handler called get_container_object("partition", 0),
   got None, and silently dropped the change. Partition events with
   area in (0, 255) are now broadcast to every known partition so the
   disarm clears state on all of them.

2. Brief "armed_away" flash on disarm from a fully-armed state. Per
   PRT3 ASCII spec page 18 G013 is "Disarm with Master", but PAI had
   it mapped as "auto-armed" (change={"arm": True, ...}). When a
   master code disarmed the panel, G013 fired and set arm=True, then
   _update_partition_states fell through to the "armed_away" else
   branch (since none of arm_stay/arm_away/arm_force were True) and
   published armed_away until the next RA poll corrected it. The arm
   group is now (G009, G010, G011, G012) and the disarm group is
   (G013, G014, G015, G016, G017) per spec. Adds previously-missing
   G009 (Arming with Master) and aligns G011/G012/G016/G017 labels to
   the spec; functional change dicts for G011/G012/G015/G016 were
   already correct.

3. 4-7 s delay before HA shows "disarmed" after a disarm during exit
   delay. The PRT3 ASCII protocol does not reliably emit a disarm
   G-event for ASCII-initiated disarms during exit delay (only G065
   N=000, which per spec is the "Ready" status flag, not an
   "exit_delay cleared" signal -- it can fire while exit delay is
   active). control_partition now applies the disarmed state
   optimistically on the panel's AD&OK echo (which is authoritative)
   and arms a 3 s freeze window during which arm-related keys from RA
   polls are dropped. The window prevents a stale RA reply (panel
   hasn't internally settled yet) from briefly re-asserting arm=True,
   and auto-expires so it can't cause a permanently stuck state.

docs/prt3-architecture.md gains an "Arm/disarm state tracking" section
explaining the three-source state model (RA polling, G-events,
optimistic command echo), the Ready-vs-exit_delay distinction, and the
HA-disarm-during-exit-delay sequence.

Also: control_partition log was promoted from DEBUG to INFO so user-
initiated commands appear in default logs, and a one-line "PRT3 system
event: G..N..A.." log was added in handle_prt3_event_message which
proved essential for diagnosing this class of issue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@NaanyaBiz
Copy link
Copy Markdown
Author

Thanks for the structured review (recorded in #597) — the multi-agent framing surfaced several things worth fixing. Before acting on the findings I traced each one against the code so we'd target the right things. Below is what I plan to act on, where I'd push back, and what I'd defer.

Findings I'll act on

# Finding Action
Bug #3 PRT3BufferFull not in _prt3_send_wait predicates Add a PRT3BufferFull predicate and fast-fail/retry on !
Bug #4 Invalid PRT3_USER_CODE raises ValueError at arm time Validate format (^\d{1,6}$) at config load, not first use
Arch #3 Empty runtime.py placeholder Delete
Minor #1 PRT3_SERIAL_BAUD accepts non-hardware values Set validator [9600, 19200] (matches CONNECTION_TYPE pattern)
Minor #2 G065 per-N overrides bypass EVENT_MAP Surface them as a "number_overrides" nested dict in EVENT_MAP
Minor #4 Bare except Exception in handle_prt3_event_message Catch expected (KeyError/AttributeError) explicitly with warning
Minor #5 User code visible if byte-level serial trace is enabled Doc note + mask in retry warning path
Minor #6 Missing tests: reconnect, buffer-full, bad config at arm time Add coverage

Findings whose impact I read differently

Bug #1assertreturn False in async_message_manager.py

For binary users only Container objects ever flow into these handlers, so neither the assert nor the guard ever fires — there's no behaviour change for them. For PRT3 users, EventMessageHandler and ErrorMessageHandler are registered alongside the PRT3 handler in paradox.py _register_connection_handlers. Without the guard, every PRT3 dataclass would trigger AssertionError on the first line of can_handle, making PRT3 mode unusable.

The architectural critique is solid though, and I'd happily take it further: register EventMessageHandler/ErrorMessageHandler only for binary connection types and a dedicated PRT3 handler only for PRT3, so the guard isn't needed at all. That fits naturally into the Arch #2 polymorphism cleanup if/when we tackle it.

Bug #2on_connect fallback args

The synthetic-None fallback exists to cover a microsecond race between self.state = CONNECTED (core.py:222) and self._last_connect_args = (...) (line 223) — the callback runs on the MQTT thread, so a registrar reading self.connected between those two assignments could see True with _last_connect_args still None. Both current on_connect overrides (basic.py:120, homeassistant.py:59) ignore reason_code, so the AttributeError path has no live target today.

That said, the suggested fix (skip replay when _last_connect_args is None) is strictly better — defensive against any future handler that does inspect reason_code. I'll change it. Fair point on the separate commit too; we can't unscramble it now but I'll keep that discipline going forward.

Bug #3 — severity scoping

Real defect, agreed on the fix. The n × IO_TIMEOUT freeze claim doesn't hold up against the code: _prt3_send_wait runs under core.request_lock and each call carries its own IO_TIMEOUT. A single buffer overflow costs one timeout (plus its retry) on the affected command, not a multi-minute cascade across load_labels. Worth fixing for clean retry semantics, not because startup will hang.

Bug #4 — severity scoping

Real defect, agreed on the fix. The "crashing the run loop" framing isn't quite right: the MQTT path is wrapped by mqtt_handle_decorator (basic.py:31-35) which catches Exception and logs; the text path runs under asyncio task supervision. The user-visible symptom is silent failure of every arm/disarm with stack traces in the log — bad UX, not a process crash. Validating at config load is still the right answer.

Minor #3control_zones / control_outputs raise instead of returning False

The orchestrator catches it: paradox.py:497-516 (control_zone) and paradox.py:630-647 (control_output) both except NotImplementedError and return False. The MQTT handler never sees the exception, so the Panel interface contract is honoured at the call site even though the panel-level method raises. I'd leave this as-is unless I'm misreading the path.

Deferring

Arch #1 (LSP) and Arch #2 (string guards) — agreed in principle, both worth doing. The concrete behaviour issues they cite are mitigated by Minor #3 above, so I'd fold the cleanup into a follow-up rather than the v1 merge: extract BinaryPanel, replace the 5 candidate guards with Panel methods, and (per Bug #1 above) wire handler registration through Panel.register_protocol_handlers(connection) so the Container guard becomes unnecessary.

Heads-up

While integration-testing on a live EVO192 panel over the last day, three orthogonal arm/disarm state bugs surfaced and are fixed in 3fe0b14e on the branch:

  • G014 with area=0 (global keypad disarm) was silently dropped because get_container_object("partition", 0) returns None. Now broadcast to all partitions.
  • G013 was mapped as "auto-armed" (arm=True); per PRT3 spec page 18 it's "Disarm with Master". When a master code disarmed, _update_partition_states fell through to the armed_away else branch.
  • HA showed a transient armed_stay state for 4-7 s after disarm-during-exit-delay because RA polls return stale armed_stay for ~hundreds of ms after AD&OK while the panel internally settles. Now applies disarmed state optimistically on &OK echo and freezes arm-key updates from RA for 3 s.

Architecture notes on the three-source state model (RA polling, G-events, command echo) added to docs/prt3-architecture.md. None of it overlaps with this review's findings, but flagging in case it changes how you'd want to sequence the merge.

Naanya Biz and others added 3 commits April 30, 2026 06:34
- Bug ParadoxAlarmInterface#2: mqtt/core.py skip on_connect replay when _last_connect_args is None
- Bug ParadoxAlarmInterface#3: panel.py _prt3_send_wait fast-fails on PRT3BufferFull instead of full timeout
- Bug ParadoxAlarmInterface#4: PRT3_USER_CODE validated at config load; _build_partition_cmd catches ValueError
- Minor ParadoxAlarmInterface#1: PRT3_SERIAL_BAUD set validator [9600, 19200]; config validator extended for int lists
- Minor ParadoxAlarmInterface#2: G065 per-N overrides in EVENT_MAP number_overrides; from_prt3 uses generic lookup
- Minor ParadoxAlarmInterface#4: handle_prt3_event_message catches expected exceptions explicitly
- Minor ParadoxAlarmInterface#5: slice comment in retry warning; expanded PRT3_USER_CODE security doc
- Minor ParadoxAlarmInterface#6: tests for buffer-full retry, malformed code, PRT3_USER_CODE/baud validation
1369 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- S112: PRT3_USER_CODE validation raises ValueError instead of Exception
- S3776: handle_prt3_event_message complexity reduced by extracting
  _apply_prt3_event_change helper method (was 16, now below 15)
- S3776: _on_status_update complexity reduced by extracting
  _filter_arm_freeze helper method (was 16, now below 15)
- S7504: remove unnecessary list() wrapping dict.keys() in event broadcast
- S7503: three sync-only test functions had spurious async keyword removed
- S1481: four tests had unused mock_panel renamed to _
1369 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- prt3-usage.md: PRT3_SERIAL_BAUD is now strictly 9600 or 19200 (not
  a free range); PRT3_USER_CODE documents the 1-6 digit format, startup
  validation, and the security note about debug log streams
- prt3-architecture.md: _prt3_send_wait buffer-full composite predicate
  and fast-retry behaviour noted; EVENT_MAP number_overrides pattern
  documented for future maintainers adding per-N group overrides

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants