Repository navigation
Architecture 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.
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
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).
| 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
|
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.
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).
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.
@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_versionLifetime is tied to the config entry, so unload cannot leak, and platform setup is one attribute read.
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.
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.
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
Four rules follow, and violating any of them produces symptoms that are hard to debug:
-
Never touch Home Assistant state from the panel thread — only
call_soon_threadsafe. - Panel state dicts are the shared memory. The panel thread writes; the event loop reads.
-
Schedule coroutines from the thread with
asyncio.run_coroutine_threadsafe, neverloop.create_task(not thread-safe). async_write_ha_state()is loop-only.
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
|
| 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 |
| 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 |
| 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 |