Skip to content

Architecture Design Document ADD

acdcnow edited this page Sep 21, 2026 · 2 revisions

Architectural Design Document (ADD)

Audience: maintainers, contributors, reviewers Applies to: branch HA2026_09_dev, version 2.1.0-beta.2

Canonical file. This page is a summary for the wiki. The full, versioned document lives in the repository and is the source of truth: docs/ADD.md. Companions: SDD · Workflows.

An Architectural Design Document answers what the system is made of and why. The SDD answers how each part is implemented.


1. Context

flowchart LR
    HA["Home Assistant"]
    Int["crowipmodule<br/>this integration"]
    Driver["vendored driver<br/>socket thread"]
    IPM["Crow IP Module<br/>serial to Ethernet"]
    Panel["Runner 8/16 or ESL-2"]

    HA --> Int --> Driver
    Driver <-->|"TCP 5002, ASCII"| IPM
    IPM <--> Panel

    classDef hw fill:#fdecea,stroke:#b03a3a
    class IPM,Panel hw
Loading

The IP Module is a serial-to-Ethernet bridge that accepts one TCP client and speaks a line-oriented ASCII protocol. Commands are COMMAND, COMMAND or COMMAND <argument> terminated \r\n; events arrive unsolicited as lines such as ZO3 (zone 3 open) or MF (mains fail). There is no request/response correlation, so every incoming line is matched against a pattern table and dispatched.

Out of scope: panel firmware, any behaviour the IP Module does not expose, and cloud access (there is none).


2. Architectural drivers

Driver Consequence
The panel pushes; polling is not viable iot_class: local_push; entities never poll
Sockets block, the event loop must not All socket I/O on one dedicated thread
The panel accepts a single connection Setup and unload both wait for socket release
The upstream PyPI driver is unmaintained Driver vendored and modernised
Home Assistant's contract evolves quickly Deprecations tracked explicitly; version pinned in hacs.json
Panel firmware variations exist Firmware version captured at setup, reported as sw_version

3. Key decisions

DEC-1 — Vendor the driver instead of depending on PyPI

pycrowipmodule (last released as a py2.py3-none-any wheel) is bundled at custom_components/crowipmodule/pycrowipmodule/ and imported relatively. The manifest declares no requirements.

Gain: installs can never fail on PyPI resolution; the protocol layer is fixable in the same commit as the integration. Cost: upstream fixes no longer arrive automatically.

DEC-2 — Blocking, threaded socket rather than asyncio streams

One worker thread owns the socket, using a blocking socket with a 5-second read timeout which doubles as the keep-alive tick.

Gain: the parser stays synchronous and simple; a stalled panel cannot stall Home Assistant. Cost: every callback must be marshalled back to the loop — the most error-prone part of the design (see §4).

DEC-3 — Dispatcher signals rather than a DataUpdateCoordinator

A coordinator models polling; this integration is pushed to, and entities have distinct interests (a zone only cares about its own ordinal). Five topics are used instead:

Signal Payload Consumers
SIGNAL_ZONE_UPDATE zone number or None CrowZoneSensor
SIGNAL_AREA_UPDATE A/B or None CrowAlarmPanel, CrowAlarmZoneSensor
SIGNAL_SYSTEM_UPDATE attribute name or None status + system sensors
SIGNAL_OUTPUT_UPDATE output number CrowOutput
SIGNAL_CONNECTION_UPDATE bool every entity (drives available)

None means "refresh everything" and is broadcast after every (re)connect.

DEC-4 — entry.runtime_data instead of hass.data

@dataclass
class CrowRuntimeData:
    controller: CrowIPAlarmPanel
    device_id: str | None = None   # hub device id, for via_device_id
    firmware: str | None = None    # "<version> (<date>)", for sw_version

Lifetime is tied to the config entry, so unload cannot leak, and platform setup is one attribute read.

DEC-5 — Register the hub device before forwarding platforms

via_device_id must be a real device-registry id, which only exists after the parent is registered. async_setup_entry therefore creates the hub device itself, stores hub_device.id, and only then calls async_forward_entry_setups.

Gain: no deprecation warning, nothing breaks in 2027.8, and zone sub-devices hang off the hub correctly. Cost: setup order becomes significant — the hub must exist before any platform runs.

DEC-6 — Fail loudly at the entity level, never silently

Every entity reports available = controller.is_connected, and the connection signal re-writes state on each transition. For an alarm system, "unknown" must never be presented as "disarmed".

Cost: a brief network blip marks entities unavailable, which some automations treat as a change.


4. Threading — the crux

sequenceDiagram
    participant P as Panel thread
    participant L as HA event loop
    P->>P: socket.recv
    P->>P: parse line, mutate panel state
    P->>P: callback_*_state_change payload
    P->>L: call_soon_threadsafe async_dispatcher_send
    L->>L: entity._update_callback
    L->>L: async_write_ha_state
Loading

Four rules follow, and violating any of them produces symptoms that are hard to debug:

  1. Never touch Home Assistant state from the panel thread — only call_soon_threadsafe.
  2. Panel state dicts are the shared memory. The panel thread writes; the event loop reads.
  3. Schedule coroutines from the thread with asyncio.run_coroutine_threadsafe, never loop.create_task (not thread-safe).
  4. async_write_ha_state() is loop-only.

5. Home Assistant 2026.9 conformance

Each row is asserted by tests/verify_ha_2026_contract.py.

Requirement Implementation
DeviceInfo uses via_device_id, not via_device (removed 2027.8) device.py::build_device_info
configuration_url must be http(s) with a host device.py::configuration_url()
Per-entry state via entry.runtime_data CrowRuntimeData
AddConfigEntryEntitiesCallback typed platform callbacks every async_setup_entry
Entities do not poll _attr_should_poll = False
AlarmControlPanelEntity.alarm_state, not legacy state CrowAlarmPanel.alarm_state
EntityCategory from homeassistant.const binary_sensor.py, sensor.py
strings.json mirrored in translations/en.json asserted by tests/verify_config_flow.py
Manifest needs domain, documentation, issue_tracker, codeowners, name, version manifest.json
Brand images may ship locally (HA 2026.3+) brand/
Python 3.14.2+ declared in hacs.json

6. Quality attributes

Attribute Mechanism Trade-off accepted
Availability available from live connection state; supervised reconnect Blips mark entities unavailable
Reliability Send failure drops the socket; setup and unload wait for release ~2 s pause per reload
Responsiveness Push-only, no polling UI rate depends on panel events
Diagnosability Structured TX:/RX: debug logs, diagnostics dump, status sensor Verbose at debug level
Security No cloud, no telemetry; host and codes redacted in diagnostics Credentials stored in the config entry
Testability Protocol layer testable with no Home Assistant; contracts pinned by harnesses Harnesses are no substitute for a live panel

7. Risks

ID Risk Impact Status
RISK-1 Unique IDs for zones/sensors/outputs/buttons are global, so two IP Modules collide Multi-panel users lose entities Open — needs a migration
RISK-2 Setup blocks ~4 s Slow startup Accepted workaround
RISK-3 is_connected read from several threads without a lock Stale reads in theory Low — CPython attribute assignment is atomic
RISK-4 async_step_import unreachable (no CONFIG_SCHEMA) Dead code Open
RISK-5 Vendored driver drifts from upstream Missed upstream fixes Accepted — upstream is dormant
RISK-6 hacs.json pins HA 2026.9.3 Blocks older installs Deliberate for the beta
RISK-7 No live-hardware CI Regressions surface only on real panels Mitigated by harnesses

8. Document map

Document Audience Contents
ADD (this) Maintainers, reviewers Architecture, decisions, runtime view, risks
SDD Implementers Modules, signatures, data models, protocol tables
Workflows Everyone Mermaid diagrams
HA 2026.09 Development Branch Users What changed, how to install
Archived Documentation Users on old releases Preserved 1.x / 2.0.0 documents

Clone this wiki locally