v0.32.0
An overhaul pass: latent bugs fixed, dead code removed, test coverage raised from 32 % to 97 % (433 → 1821 tests), and entity/device identifiers namespaced per account. mypy --strict clean, ruff clean, validated on a live install.
⚠️ To be aware of
- Entity & device identifiers are now namespaced per config entry (schema v1.2 → v1.3). This makes it possible to run multiple Ajax accounts in one Home Assistant without entity/device collisions. Existing setups migrate automatically on upgrade: each
unique_idis renamed in place, so yourentity_ids, history, dashboards and automations are preserved — no action required. (Single-account setups see no functional change.)
Added
- Comprehensive unit-test suite: coverage 32 % → 97 %, with a migration "guarantee" test pinning that the v1.3 migration reproduces the exact runtime
unique_idformat (so the in-place rename can never orphan an entity).
Fixed
- Motion detection was never reported in direct (SQS) mode. The SQS motion handler wrote the attribute
motionwhile the motion binary sensor readsmotion_detected(the key the SSE handler already writes), so a real-time motion event never turned the sensor on when armed in direct mode. It now writesmotion_detected(+motion_detected_at). - Diagnostics download crashed for every proxy / SSE user.
_connectivity_snapshottried to callAjaxSSEClient.is_connected, which is a@propertyreturning abool—TypeError: 'bool' object is not callableaborted the whole download. It now reads the property without calling it. - CombiProtect glass-break was silent in direct (SQS) mode. Its glass-break sensor only read the SSE key
glass_break_detected; SQS writesglass_alarm. It now reads both (matching the standalone GlassProtect sensor). - Door-sensor fast-polling could silently die. The background loop iterated
account.spaceswhile awaiting an API call; a hub discovered concurrently raisedRuntimeError: dictionary changed size during iterationand killed the task until reload. The loop now snapshots the spaces first. - DoorProtect Plus / GlassProtect config switches bounced back. The shock-sensor / accelerometer / extra-contact / ignore-simple-impact / blink-while-armed switches set an optimistic state, but the poller overwrote it without checking the optimistic guard. The poller now honours the reservation (parity with the other config switches).
- Doorbell "Last ring" sensor went
unknownon every ring. TheTIMESTAMPsensor received an ISO string; Home Assistant requires adatetime. The stored value is now parsed back to adatetime. - Duplicate hub-battery sensor.
hub_batterywas defined twice (space-level and hub-level), producing a collidingunique_id; Home Assistant silently dropped one entity and logged an error on every start. It is now defined once. - Manual Call Point colour sensor errored for some colours. The
ENUMoptions listed only red/blue/white/black; a yellow/green/graphite device raisedValueError. The full Ajax colour set is now declared (with translations in all 7 languages). - Socket external-power monitoring was wired to the wrong key. The socket sensor gated/read
external_power(never populated) while the REST poller writesexternally_powered, and the SSE power event wrote a third, dead key. All three now agree onexternally_powered. - Auth failures on the rooms endpoint bypassed reauth.
async_get_roomserrors were swallowed by a broadexcept, so a token expiry surfacing there never counted toward the reauth threshold.AjaxRestAuthErrornow propagates (parity with the hub/users/groups fetches). - External Contact entity lagged ~30 s in proxy mode (#151).
extcontactopened/extcontactclosedreal-time events were routed through the door handler and wrotedoor_opened, so the External Contact binary sensor only ever updated from the REST poll. They now writeexternal_contact_opened(matching the REST poll and the sensor's value_fn) in both the SSE and SQS managers — real-time again. - Smoke detector high-temperature alarm ignored real-time SSE. The
high_temperaturebinary sensor only read the RESTtemperatureAlarmDetectedkey and missed the SSEtemperature_alertkey, so a temperature alarm only showed up at the next poll. It now reads both. - Dimmer switches bounced back after toggling.
AjaxDimmerSettingsSwitchandAjaxDimmerBoolSwitchapplied an optimistic state without guarding it, so a poll landing within ~1 s reverted the toggle. The state is now reserved for 15 s and cleared on the error rollback (parity with the other switches). - A malformed
modelpayload could abort a whole refresh. Device reconciliation now guards against a non-objectmodelfield instead of raisingTypeErrorand halting the poll cycle. - Hardening:
get_nvr_recordingsno longer dereferences a not-yet-populated coordinator; the API client raises a clear "not logged in" error (instead of building auser/None/…URL) on the device/camera endpoints; the SSE "alarm triggered by motion" log now shows the previous state rather than the new one. - Deprecation warnings cleared:
TrackerEntityis imported from its public path, and the firmware-update entity passeshw_versionas a string — both silencing Home Assistant 2026.x deprecation notices. - Real-time SQS events recovered after a deferred
aiobotocoreinstall. Whenaiobotocorewas pulled in lazily on first run, the SQS client failed to start and real-time events stayed off until a reload; it now connects automatically once the dependency is available.
Changed
- Internal modularisation (no behaviour change, no
unique_idchange — verified against a live install). The largest modules were split along clean responsibility lines so the codebase is easier to maintain:- SSE/SQS event-mapping tables →
event_maps.py(a single source of truth shared by both managers, removing thesse_manager → sqs_managerimport). coordinator.py→ door-sensor fast-polling extracted to_coordinator_door_poll.py(AjaxDoorPollingMixin)._coordinator_devices.py→ stateless attribute normalisation + motion-impulse expiry extracted to_device_normalize.py.switch.py→ entity classes split into_switch_entity.py(generic) and_switch_dimmer.py(LightSwitchDimmer).config_flow.py→ options flow extracted toconfig_flow_options.py.__init__.py→ service registration extracted to_services.py.
- SSE/SQS event-mapping tables →
- Test coverage raised 97 % → 98 % (1833 tests), notably bringing
__init__(setup entry) andcoordinator(constructor) to ~100 %. - French translation polish: corrected an anglicism, added the missing French space before colons in the setup/options dialogs, and hyphenated the French e-mail term.
Removed
- Dead code: unused API client methods (
async_control_device,async_set_light_state,async_get_nvr_status), an unreferenced notification parser,sse_client.update_session_token,onvif_manager.get_client, and abandoned poll-count instrumentation in the SQS client.