v0.31.0
A full code-review pass over the whole integration: 46 confirmed findings fixed, each adversarially verified, plus four agent-introduced regressions caught and corrected during a diff-verification pass. mypy --strict clean (58 files), ruff clean, 342 tests (was 328).
⚠️ Behaviour changes to be aware of
- WaterStop diagnostic sensor states changed.
motor_statenow reportsoff/rotate_to_closing/rotate_to_open(wasoff/on) andexternal_powerreportssupply/no_supply(wassupply/battery/unknown), matching the real AjaxmotorState/extPowerAPI enums. Automations or history templates keyed on the oldon,batteryorunknownstates will need updating. - SQS alarm-notification default flipped. An unset notification-filter option now defaults to "all" instead of "none". If you never set a filter and currently receive no alarm notifications, you will start receiving them after this upgrade (only an explicit "none" suppresses them).
- SSE / proxy mode now produces alarm history and persistent notifications. Motion, smoke/fire, flood and glass-break events — and a door opened while armed — now append to a space's recent-events history and raise a persistent notification, matching SQS behaviour. Proxy-mode users will start seeing these.
- Devices survive a partial poll. A device missing from a single non-empty
200response is no longer deleted immediately; it must be absent for 3 consecutive polls first. A truncated proxy response no longer wipes devices (stale devices may linger up to ~3 polls longer). - Hub "external power" binary sensor now reads the dedicated
externallyPoweredfield instead of inferring frombattery.state != DISCHARGED; the reported state may change for some hubs and now reports even when no battery data exists. - Video Edge, Video Edge sensor and firmware-update entities go unavailable during failed coordinator updates instead of showing stale values.
Added
- Live discovery of spaces and groups. A space or group added in Ajax after setup now creates its alarm-panel entity at runtime via the new
SIGNAL_NEW_SPACE/SIGNAL_NEW_GROUPdispatcher signals — no integration reload required (mirrors the existing new-device path). - SSE / proxy-mode alarm history & notifications for motion, smoke/fire, flood, glass-break and door-while-armed (parity with the SQS transport). Door-while-armed records history without forcing the space to
TRIGGERED, mirroring SQS. arm_failedevent message added toEVENT_MESSAGESin fr / en / es / de / nl / sv / uk (used by the unsuccessful-arming event code).- "Unknown" label for the Video Edge storage-status sensor across
strings.jsonand all 7 languages, matching the code's fallback value.
Fixed
- Cross-hub event suppression (multi-hub). Replaced the coordinator's single global
_skip_state_change_eventboolean with a per-hub_skipped_state_change_hubsset, so a metadata refresh on one hub no longer drops the REST-side arm/disarm state-change event for every other hub processed in the same tick. - Night-mode unavailability (#149 class). A transient
async_get_hubfailure inside the per-hub loop leftis_new_space/real_space_id/space_nameunbound, raisingUnboundLocalErrorand flipping the whole refresh to failure (all entities unavailable). The except handler now initialises them so the space degrades gracefully (created asNONE, or kept) instead of crashing the tick. - Optimistic arm/disarm reverted by a stale poll. Space-level state updates now honour
has_pending_ha_action(the sameha_protectedguard the group path already had), so a REST poll landing inside the ~10 s window no longer snaps the panel back to its previous state after a user command. - SSE second-event misattribution. The SSE manager now peeks the pending-HA-action flag (
has_pending_ha_action) instead of consuming it, so when Ajax emits two state events for one HA command (e.g.armthenarmwithmalfunctions) both stay attributed to Home Assistant rather than the second firing a misleading "armed/disarmed by " notification. - Optimistic LED / brightness reverted by polling. Siren
led_indicationand dimmeractualBrightnessCh1are now only overwritten from the API when not inside their optimistic-update guard; a poll within the TTL window no longer reverts the user's change. The siren blink-while-armed switch maps its optimistic write toled_indication(the attribute it actually reads), and multi-gang LightSwitch tracks the optimistic guard per channel so one channel's poll can't overwrite another's in-flight change. - Cache bypass missed most of the refresh.
bypass_cache_next()set a one-shot boolean consumed by the first request of a cycle (the hub list), so device and space getters still served stale data. It now opens a short 2 s window covering the whole refresh cycle, and the space getter (which feeds cameras and smart locks) honours it too — entity state after an SSE/SQS event or user action is now fresh, not TTL-lagged. Retry-AfterHTTP-date crashed the back-off path. A bareint(Retry-After)raisedValueErrorwhen a server sent an RFC 7231/9110 HTTP-date on a429, crashing the retry exactly when asked to back off. A new_parse_retry_after()handles both seconds and dates, clamps to non-negative, and falls back to 60 s.- Panel state not restored on a failed command. A failed arm/disarm/night-mode on space and group panels now captures and restores the previous state synchronously (
async_write_ha_state) before the debounced refresh, instead of showing the wrong optimistic state during the debounce window. - Night-mode arm switch never worked on LightSwitch / Transmitter. Both read the camelCase
nightModeArm, but the coordinator only stores the snake_casenight_mode_armalias — so the switch was never created on LightSwitch and always readFalseon Transmitter. Both now usenight_mode_arm. - Siren alarm-duration showed an invalid state.
alarmDurationis reported in minutes (not seconds); an earlier/60produced an off-list0. The select now maps 1:1 and snaps any off-list hub value to the nearest of1/2/3/5/10/15. - FireProtect2 CO siren-trigger switch snapped back. The CO switch read either
CO/CCOtoken but the write only removedCO, so turning it off left the other token and the read flipped it back on. It now uses one token (preferCO, elseCCO) for both directions. - Video Edge channel lookup returned the wrong channel.
_get_channel_by_idnow does two passes (explicit-id match, then positional fallback) so out-of-order channel ids — e.g. after a camera is removed — no longer resolve to a neighbour by index. - Video Edge enum sensors emitted raw API values. Record-mode, record-policy and storage-status now fall back to the in-options
unknowninstead ofvalue.lower()for unmapped enums. The boot-time sensor is normalised to whole minutes so it stops jittering by seconds every poll and inflating recorder history. - Per-room device counts went stale.
room.device_idsis now rebuilt every poll (it was append-only), so a device moved between rooms or deleted no longer inflates the per-roomdevice_countshown in panel attributes. - Shared door-sensor polling stopped too early. The shared polling task now only stops when no space is disarmed or in night mode, instead of being cancelled when any one space armed — other disarmed spaces keep getting timely door updates.
- Auth errors now drive reauth.
AjaxRestAuthErroris re-raised (not swallowed) in the video-edge and smart-lock updaters, so token expiry counts toward the reauth threshold instead of being logged and ignored. - Orphaned ONVIF clients. On a partial ONVIF init failure, already-connected clients are stopped via
async_stop()before the reference is dropped, preventing orphaned poll tasks / PullPoint subscriptions. - Colliding notification IDs. Arm/disarm persistent-notification ids now include the space and source name, so concurrent events no longer overwrite each other in the notifications panel.
- Arming-failure event misclassified. Removed the
M_22_21 → TRIGGEREDoverride; it now resolves toRECOVEREDvia the odd/even hex heuristic, so an arming failure is no longer surfaced as a one-shot triggered state. - ONVIF detection zone name dropped on secondary events. The rule/zone name is now threaded into
_parse_eventso every emitted detection event (including secondary AI object-detection, motion, line-crossing and doorbell events) carries it, not just the single returned event. - Diagnostics SQS connectivity is derived from the live receiver thread and queue URL (the old
is_connected()call didn't exist and always readFalse). - Diagnostic dumps redacted.
ajax_nvr_recordings.jsonandajax_smart_locks.jsonnow pass throughasync_redact_data, and the diagnosticsTO_REDACTset additionally coversaddress,ssid,gateway,netmask,dnsandnetworkInterface. - Flood-detector malfunctions sensor simplified to the normalised int count (the list/string branches were dead code).
Changed
- Force-arm services route through the coordinator.
force_arm/force_arm_nightnow callcoordinator.async_arm_space/async_arm_night_mode(force=True)(instead of the raw API), apply across all matched config entries, and report aggregated per-hub failures (raisinginvalid_targetwhen nothing resolves) — multi-account users can drive them across every hub. - Diagnostic services span all entries.
get_raw_devices, force-metadata-refresh, NVR-recordings and smart-lock dump handlers now iterate every matched config entry instead of only the first (they simply run for each; no failure aggregation). - Night-mode fast-poll filter default flipped so door/transmitter/wire-input sensors that don't report
night_mode_armare now included in night-mode fast polling (faster updates for those sensors in night mode).
Removed
devices/dimmer.pydead handler methods (get_switches/get_numbers/get_selects, ~280 lines): dimmer entities are built from static definitions in the platform files gated byis_dimmer_device(), so these were never invoked.AjaxDevice.is_triggeredandlast_notification: dead code (trigger state is tracked via per-attribute flags +last_trigger_timein the SSE/SQS managers).- Per-door-sensor fast-poll mechanism (
_fast_poll_tasks,_async_fast_poll_door_sensorand its teardown): superseded by the shared continuous door-sensor polling task.
Tests
- +14 regression tests (328 → 342). New coverage for:
Retry-Afterdate parsing and the cache-bypass window (test_api_helpers.py); the night-mode runtime-import and hub-detailsUnboundLocalErrorpaths (test_coordinator_spaces_nightmode.py); the transmitter / siren / WaterStop / Video Edge handler fixes (test_device_handlers.py); the Video Edge channel id-vs-index lookup (test_models_extras.py). - New ENUM-options ⇄ translation parity guard (
test_translations.py): scansdevices/*.pyand fails if any literaloptionslist has a key without a matching translatedstateentry instrings.json— the exact class of bug (WaterStop / Video Edge) caught in this pass.