Skip to content

Software Design Document SDD

acdcnow edited this page Sep 21, 2026 · 2 revisions

Software Design Document (SDD)

Audience: implementers 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/SDD.md. Companion: Architectural Design Document · Workflows.

A Software Design Document answers how each part is built. The ADD answers what the system is made of and why.


1. Module inventory

Module Responsibility
__init__.py Entry setup/teardown, callback marshalling, hub device registration
device.py CrowRuntimeData, configuration_url(), build_device_info()
const.py Domain, option keys, limits, defaults, signal topics, device identifiers
config_flow.py Config flow (5 steps) + options flow (4 steps)
alarm_control_panel.py CrowAlarmPanel
binary_sensor.py CrowZoneSensor, CrowSystemStatusSensor
button.py CrowChimeButton, CrowRelayButton
sensor.py CrowSystemSensor, CrowAlarmZoneSensor
switch.py CrowOutput
diagnostics.py Redacted config-entry diagnostics
pycrowipmodule/ Vendored protocol driver — must not import homeassistant

2. Configuration data model

entry.data

Key Default Notes
host — IP address or hostname of the IP Module
port 5002
keepalive_interval 300 Seconds between STATUS probes
timeout 10 TCP connect timeout
firmware_version / firmware_date Ver 2.10.3628 2017 / Oct 20 09:48:43 Date derived from FIRMWARE_PROFILES
number_of_areas 2 max 2
number_of_zones 7 max 16
number_of_outputs 2 max 8, 0 allowed
arm_sequence command_then_keypad command_then_keypad · command_only — see §3.6

entry.options

{
  "areas":   { "1": { "name": "House", "code": "1234", "code_arm_required": true } },
  "outputs": { "1": { "name": "Gate" } },
  "zones":   { "1": { "name": "Front Door", "type": "door" } }
}

All keys are stringified ordinals to survive JSON round-tripping. type ∈ window · motion · door · smoke · gas · co · tamper · safety.


3. Protocol reference

3.1 Framing

Aspect Rule
Transport TCP, default port 5002
Encoding ASCII
Terminator Outgoing \r\n; incoming split on \r\n, falling back to \n
Read size / timeout 1024 bytes; _RECV_TIMEOUT = 5.0 s (a timeout is the keep-alive tick, not an error)

3.2 Commands

Key Wire Argument Used by
arm ARM — arm_away()
stay STAY — arm_stay()
disarm KEYS <code>E disarm(code)
keys KEYS <code>E send_keypress(code)
panic PANIC — panic_alarm()
toggle_chime CHIME — toggle_chime(), button
relay_1_on / relay_2_on RL1 / RL2 — relay_on(n), buttons
toogle_output_x OO <n> command_output(n), switch
status STATUS — handshake + keep-alive
quick_arm_a / quick_arm_b A / B — defined, unused
get_memory_events / exit_memory_events MEME / MEMX — defined, unused

Wire formatting:

data == ""    ->  "<CMD> "        "STATUS ", "ARM ", "CHIME "
cmd == "OO"   ->  "OO<n>"         no space
otherwise     ->  "<CMD> <data>"  "KEYS 1234E"

3.3 Responses

First re.match wins. The handler mutates panel.<x>_state and returns the payload passed to the callback.

Zones — data = zone number, callback callback_zone_state_change

Pattern Event Attribute Value
ZO<d> / ZC<d> Zone open / closed open True / False
ZA<d> / ZR<d> Alarm / restore alarm True / False
ZBY<d> / ZBYR<d> Bypass / restore bypass True / False
ZTA<d> / ZTR<d> Tamper / restore tamper True / False

Areas — fixed letter, callback callback_area_state_change

Literal Attribute Area
AA / AB armed = True A / B
SA / SB stay_armed = True A / B
DA / DB disarmed = True A / B
EAA / EAB exit_delay = True A / B
ESA / ESB stay_exit_delay = True A / B

Area messages are exclusive: the other flags are cleared first. D* also clears alarm and alarm_zone.

Outputs — callback callback_output_state_change: OO<d> sets open = True, OC<d> sets open = False.

System — callback callback_system_state_change

Fail Restore Attribute
MF MR mains
BF BR battery
TF TR tamper (fail → True)
LF LR line
DF DR dialler
FF FR fuse
ZBF ZBR zonebattery
PBF PBR pendantbattery
CTF CTR codetamper (fail → True)
— RO / NR ready

TF stores True, matching BinarySensorDeviceClass.TAMPER semantics where "on" means sabotage detected. Note the asymmetric polarity versus the fail/restore pattern.

3.4 State shapes

zone_state[n]   = {"status": {"open": False, "bypass": False, "alarm": False, "tamper": False},
                   "last_fault": 0}
area_state[n]   = {"status": {"alarm": False, "armed": False, "stay_armed": False,
                              "disarmed": False, "exit_delay": False, "stay_exit_delay": False,
                              "alarm_zone": "", "last_disarmed_by_user": "",
                              "last_armed_by_user": ""}}
output_state[n] = {"status": {"open": False}}
system_state    = {"status": {"mains": True, "battery": True, "tamper": False, "line": True,
                              "dialler": True, "ready": True, "fuse": True,
                              "zonebattery": True, "pendantbattery": True, "codetamper": False}}

Default polarity: mains/line/dialler/battery are True (healthy); tamper flags are False.

3.5 Known protocol asymmetry

OO<n> is a toggle, not "set on". The switch platform caches the last known state and guards its own calls so a repeated turn_on is a no-op, which prevents an accidental double-toggle.

3.6 Arming sequence (arm_sequence)

There is no "arm with code" command. Depending on the firmware and how the panel is programmed, either the bare ARM / STAY completes the arming, or the panel then waits for the user code followed by Enter. Both behaviours exist in the field, so the integration makes it a per-entry option:

arm_sequence Wire sequence Rationale
command_then_keypad (default) ARM (or STAY ) then KEYS <code>E The panel arms only once the code and Enter have been entered. Matches 2.0.0.
command_only ARM (or STAY ) only The panel arms immediately, and a following code press would be read as a disarm.

send_keypress(code) writes KEYS <code>E, where the trailing E is the ENTER key — so a single keypress line is the keypad equivalent of typing the code and pressing Enter. That line is byte-identical to the one disarm() sends, which is why the same wire format serves both "finish arming" and "disarm": the panel decides from its current state.

<code> is the code argument when the caller supplies one, otherwise the area code from the area options. If command_then_keypad is selected but no code is available, the arm command is still sent and a warning is logged. Disarming is always keypad style, independent of arm_sequence; with no code available it logs an error and sends nothing.

arm_sequence is rendered by a SelectSelector with translation_key="arm_sequence", so its option labels live in the top-level selector.arm_sequence.options block of strings.json and all five translations rather than in the step's data section.


4. Entity reference

Platform Class unique_id Device Key state mapping
alarm_control_panel CrowAlarmPanel {entry_id}_crow_area_{n} hub alarm→TRIGGERED, armed→ARMED_AWAY, stay_armed→ARMED_HOME, exit delays →ARMING, disarmed→DISARMED; arming follows arm_sequence (§3.6)
binary_sensor CrowZoneSensor crow_zone_{n} Windows / Doors / Sensors sub-device zone_state[n].status.open
binary_sensor CrowSystemStatusSensor crow_sys_{key} hub see below
sensor CrowSystemSensor crow_system_status_text hub ALARM · Armed Away · Armed Stay · Exit Delay · Stay Exit Delay · Power Failure · Low Battery · Ready
sensor CrowAlarmZoneSensor crow_alarm_zone_area_{n} hub "Zone <n>" from alarm_zone, else "None"
switch CrowOutput crow_output_{n} hub output_state[n].status.open
button CrowChimeButton crow_toggle_chime hub — (CHIME)
button CrowRelayButton crow_relay_{n} hub — (RL1 / RL2)

System status sensors (entity_category = DIAGNOSTIC):

unique_id Name Device class is_on
crow_sys_mains Mains Power POWER mains
crow_sys_battery System Battery BATTERY not battery
crow_sys_tamper System Tamper TAMPER tamper
crow_sys_line Phone Line CONNECTIVITY line
crow_sys_dialler Dialler CONNECTIVITY dialler
crow_sys_zonebattery Zone Battery BATTERY not zonebattery

Battery classes are inverted because the protocol reports battery OK while Home Assistant expects on = problem. Missing keys default to True (healthy).

Zone type → device class: window→WINDOW, door→DOOR, motion→MOTION, smoke→SMOKE, gas→GAS, co→CO, tamper→TAMPER, safety→SAFETY. Unknown types log a warning and get no device class.

Shared behaviour: should_poll = False, has_entity_name = True, available = controller.is_connected, and device_info always built by build_device_info().


5. Device registry

Device Identifier Model Linked to hub
Crow Alarm System crow_alarm_panel IP Module — (the hub)
Crow Alarm Windows crow_windows IP Module Zone via_device_id
Crow Alarm Doors crow_doors IP Module Zone via_device_id
Crow Alarm Sensors crow_sensors IP Module Zone via_device_id

Identifier strings are frozen. Changing one duplicates every existing device.


6. Config and options flow

Step Fields
user host, port, keepalive_interval, timeout, firmware_version, number_of_areas, number_of_outputs, number_of_zones
areas area_{i}_name (required), area_{i}_code (optional)
outputs output_{i}_name — skipped entirely when the output count is 0
zones zone_{i}_name, zone_{i}_type — 4 zones per page (PAGE_SIZE = 4)
  • Unique ID is "{host}_{port}"; a duplicate aborts with already_configured.
  • A blocking TCP probe (socket.create_connection, run in the executor) runs before the entry is created. Failure renders cannot_connect; no entry is created.
  • The options flow uses the framework-provided self.config_entry and takes no constructor arguments (the pattern supported since OptionsFlow(config_entry) was deprecated). Finishing it reloads the entry.
  • strings.json is mirrored byte-for-byte into translations/en.json; de, es, fr, it cover every step.

7. Error-handling policy

Layer Policy
Parser Unknown lines ignored at debug level; never raises
Handlers / callbacks Wrapped in try/except, logged, never propagate (protects the worker thread)
Command send Failure closes the socket so the supervisor reconnects
Entity commands try/except + _LOGGER.error; never raise into the service call
Setup Constructor failure → False; probe failure → form error
Unload Must not raise; guards a missing runtime_data

8. Verification

Layer Artefact Covers
Protocol test_crow_protocol.py Command wire formats, RX parsing, state handlers, loopback socket
HA contract tests/verify_ha_2026_contract.py Every platform's setup; no via_device; via_device_id linkage; all seven alarm_state values; configuration_url sanitising
Config flow tests/verify_config_flow.py Full 5-step flow, pagination, options flow, translation coverage
Static python -m compileall custom_components Syntax
python -m compileall custom_components
python test_crow_protocol.py
python tests/verify_ha_2026_contract.py
python tests/verify_config_flow.py

Install voluptuous into tests/vendor (or any importable location) to run the config-flow harness.


9. Contributor conventions

  1. pycrowipmodule/ must stay free of homeassistant imports.
  2. Never call Home Assistant APIs from the panel thread — use _thread_safe_send or run_coroutine_threadsafe.
  3. Build device_info only via build_device_info().
  4. Do not change IDENTIFIER_* constants or existing unique_ids without a migration plan.
  5. Every new user-visible string needs strings.json and all five translations.
  6. Run all four verification commands before opening a pull request.

Clone this wiki locally