-
Notifications
You must be signed in to change notification settings - Fork 11
Provider State Management
This guide explains how Lock Code Manager providers manage usercode state, including update modes, the coordinator lifecycle, and best practices.
The LockUsercodeUpdateCoordinator manages usercode state through three update modes:
| Mode | Mechanism | When to Use |
|---|---|---|
| Poll for updates | Periodic get_usercodes()
|
Default for most integrations |
| Push for updates | Real-time subscription | Integrations with event support |
| Poll for drift | Periodic hard_refresh_codes()
|
Detect out-of-band changes |
All modes include an initial poll to populate coordinator data.
Important: Even with push mode enabled, you must implement
get_usercodes(). The coordinator always calls it for the initial data load and any manual refresh requests.
┌─────────────────────────────────────────────────────────────────┐
│ LockUsercodeUpdateCoordinator │
├─────────────────────────────────────────────────────────────────┤
│ data: dict[int, int | str] # slot -> usercode │
├─────────────────────────────────────────────────────────────────┤
│ async_get_usercodes() # poll method │
│ push_update(updates) # push entry point │
│ _async_drift_check() # hard refresh timer │
│ _async_connection_check() # connection poll timer │
└─────────────────────────────────────────────────────────────────┘
│
│ calls
▼
┌─────────────────────────────────────────────────────────────────┐
│ BaseLock │
├─────────────────────────────────────────────────────────────────┤
│ Properties: │
│ supports_push: bool # opt-in for push mode │
│ usercode_scan_interval: timedelta # polling interval │
│ hard_refresh_interval: timedelta # drift detection │
│ connection_check_interval: timedelta # connection polling │
├─────────────────────────────────────────────────────────────────┤
│ Required Methods: │
│ get_usercodes() # return current codes │
│ set_usercode() # set a code on lock │
│ clear_usercode() # clear a code from lock │
│ is_connection_up() # check lock connectivity │
├─────────────────────────────────────────────────────────────────┤
│ Optional Methods: │
│ hard_refresh_codes() # re-fetch from device │
│ subscribe_push_updates() # set up event listeners │
│ unsubscribe_push_updates() # clean up listeners │
└─────────────────────────────────────────────────────────────────┘
│
│ implements
▼
┌─────────────────────────────────────────────────────────────────┐
│ YourLockProvider │
└─────────────────────────────────────────────────────────────────┘
The coordinator periodically calls get_usercodes() at the interval specified by usercode_scan_interval.
Flow:
- Timer fires at
usercode_scan_interval - Coordinator calls
async_internal_get_usercodes() - Base class applies rate limiting, then calls
async_get_usercodes() - Provider returns current usercode state
- Coordinator updates
dataand notifies listening entities
When to use:
- Integrations without real-time event support
- Simple implementations where polling is sufficient
Example configuration:
class MyLock(BaseLock):
@property
def usercode_scan_interval(self) -> timedelta:
return timedelta(minutes=1) # Poll every minute
@property
def hard_refresh_interval(self) -> timedelta | None:
return None # No drift detection neededFor integrations that support real-time events (e.g., Z-Wave JS value updates), push mode provides immediate updates without polling overhead.
Flow:
- Provider subscribes to device events in
subscribe_push_updates() - Device event fires (e.g., code changed)
- Provider's event handler calls
coordinator.push_update({slot: value}) - Coordinator merges update into
data - Coordinator notifies listening entities immediately
When to use:
- Integrations with real-time event support
- When you want immediate UI updates after code changes
Example configuration:
class MyLock(BaseLock):
@property
def supports_push(self) -> bool:
return True # Disable polling, use push instead
@property
def hard_refresh_interval(self) -> timedelta | None:
return timedelta(hours=1) # Still check for driftDrift detection catches out-of-band changes (e.g., codes changed at the lock's keypad, or via another integration).
Flow:
- Timer fires at
hard_refresh_interval - Coordinator calls
async_internal_hard_refresh_codes() - Provider queries the device directly, bypassing cache
- If data differs from current state, coordinator updates and notifies entities
When to use:
- Codes can be changed outside Home Assistant
- Integration caches data that may become stale
- You want to catch sync issues even with push mode
| Property | Default | Purpose |
|---|---|---|
usercode_scan_interval |
1 minute | How often to poll for usercode updates (ignored if supports_push=True) |
hard_refresh_interval |
None |
How often to hard refresh for drift detection (None = disabled) |
connection_check_interval |
30 seconds | How often to check connection state (None = disabled) |
class MyLock(BaseLock):
@property
def usercode_scan_interval(self) -> timedelta:
return timedelta(minutes=1)
@property
def hard_refresh_interval(self) -> timedelta | None:
return None # No drift detectionclass MyLock(BaseLock):
@property
def supports_push(self) -> bool:
return True
@property
def hard_refresh_interval(self) -> timedelta | None:
return timedelta(hours=1) # Periodic drift check
@property
def connection_check_interval(self) -> timedelta | None:
return None # Z-Wave JS provides config entry state changesclass MyLock(BaseLock):
@property
def usercode_scan_interval(self) -> timedelta:
return timedelta(minutes=1)
@property
def hard_refresh_interval(self) -> timedelta | None:
return timedelta(hours=1)@property
def supports_push(self) -> bool:
return True@callback
def subscribe_push_updates(self) -> None:
"""Subscribe to real-time value updates.
Must be idempotent - no-op if already subscribed.
"""
# Skip if already subscribed
if self._unsub is not None:
return
@callback
def on_code_changed(slot: int, usercode: str | None) -> None:
# Convert to coordinator format (empty string for cleared)
value = usercode if usercode else ""
# Push update to coordinator
if self.coordinator:
self.coordinator.push_update({slot: value})
# Store unsubscribe function for cleanup
self._unsub = self.device.subscribe_to_code_events(on_code_changed)@callback
def unsubscribe_push_updates(self) -> None:
"""Unsubscribe from value updates.
Must be idempotent - no-op if already unsubscribed.
"""
if self._unsub:
self._unsub()
self._unsub = NoneProviders must raise LockCodeManagerError subclasses for lock communication failures:
from ..exceptions import LockDisconnected
def get_usercodes(self) -> dict[int, int | str]:
try:
return self._fetch_codes()
except SomeDeviceError as err:
raise LockDisconnected("Cannot communicate with lock") from errAvailable exceptions:
| Exception | When to Use |
|---|---|
LockDisconnected |
Lock is unreachable or communication failed |
LockCodeManagerError |
Base class for other LCM errors |
The coordinator catches LockCodeManagerError and handles retry logic. Do NOT raise generic
exceptions or HomeAssistantError directly.
The base class provides automatic rate limiting through _execute_rate_limited(). Use the
async_internal_* methods which apply rate limiting:
-
async_internal_get_usercodes()- rate-limited get -
async_internal_set_usercode()- rate-limited set + refresh -
async_internal_clear_usercode()- rate-limited clear + refresh -
async_internal_hard_refresh_codes()- rate-limited hard refresh
The default delay between operations is 2 seconds (MIN_OPERATION_DELAY).
The base class handles connection state transitions:
-
Reconnection detection: When
is_connection_up()transitions fromFalsetoTrue:- Coordinator refresh is triggered
- Push subscriptions are re-established (if
supports_push=True)
-
Disconnection handling: When
is_connection_up()transitions fromTruetoFalse:- Push subscriptions are cleaned up (if
supports_push=True)
- Push subscriptions are cleaned up (if
-
Config entry state changes: For integrations that expose config entry state (like Z-Wave JS):
- The base class listens for state changes
- Automatically resubscribes when the integration reloads
-
Prefer push mode when your integration supports events - it's more responsive and reduces device traffic.
-
Use drift detection if codes can be changed outside Home Assistant (e.g., at the lock's keypad).
-
Cache appropriately -
get_usercodes()can return cached data, buthard_refresh_codes()should always query the device. -
Handle disconnections gracefully - raise
LockDisconnectedrather than letting exceptions bubble up. -
Clean up subscriptions - always implement
unsubscribe_push_updates()to prevent memory leaks. -
Make subscriptions idempotent -
subscribe_push_updates()andunsubscribe_push_updates()may be called multiple times. -
Return change indicators -
set_usercode()andclear_usercode()should returnFalseif no change was made to avoid unnecessary refreshes. -
Use the internal methods - Call
async_internal_*methods which provide rate limiting, connection checks, and automatic coordinator refresh.