Skip to content
raman325 edited this page Mar 30, 2026 · 1 revision

Lock Code Manager Architecture

Overview

Lock Code Manager (LCM) manages user codes across locks via providers. Each lock gets a coordinator that holds the full slot-to-code mapping (managed AND unmanaged).

Data Flow

Config Entry (desired state)
    |
Binary Sensor (sync decision)
    | set/clear
Provider (async_set_usercode / async_clear_usercode)
    | Z-Wave service call
Lock (firmware)
    | push notification / poll response
Coordinator (actual state: ALL slots)
    | listener notification
Binary Sensor (re-evaluate sync)

Coordinator

Stores dict[int, int | str] mapping slot number to code for ALL slots on the lock. Does not distinguish managed vs unmanaged — that distinction lives in the config entries.

  • Push-based providers (Z-Wave JS) set update_interval = None
  • Poll-based providers use periodic refresh via _async_update_data()

Push vs Poll vs Hard Refresh

  • Push: Z-Wave value update events → provider filters → coordinator.push_update()
  • Poll: Coordinator's _async_update_data() → provider's async_get_usercodes() on interval
  • Hard refresh: async_hard_refresh_codes() → refreshes Z-Wave CC values cache from device, then reads all slots. Triggered via the hard_refresh_usercodes service.

Managed vs Unmanaged Slots

A slot is managed if it exists in any LCM config entry's CONF_SLOTS for this lock AND the corresponding LCM entities exist.

Detection: _get_slot_entity_states() returns None for unmanaged slots.

Coordinator stores both managed and unmanaged; sync only operates on managed slots; the UI displays both.

Sync Decision

The in-sync binary sensor compares desired state (PIN from text entity, active state from condition) against actual state (coordinator data):

  • If active + PIN differs from coordinator → set
  • If inactive + code present in coordinator → clear
  • If states match → in sync (no operation)

PIN Clearing Auto-Disables Slots

When a user clears the PIN text entity on an enabled slot, the text entity automatically disables the slot (turns off the enabled switch) before clearing the PIN value. This ensures the sync logic will clear the code on the lock rather than leaving a stale code active.

Masked PINs

Some locks return **** instead of actual codes. LCM handles this:

  • Managed slots: Resolved via the text entity's expected PIN — if the expected PIN is set and the coordinator shows ****, the binary sensor treats it as in-sync (the lock has a code, and we set it, so we trust it matches).
  • Unmanaged slots: Kept as-is — **** just indicates "slot in use."

Duplicate Code Detection

Two layers of defense against duplicate PINs causing infinite sync loops:

  1. Pre-flight check (_check_duplicate_code() in BaseLock): Scans coordinator data for matching PINs before sending to the lock. Raises DuplicateCodeError. Skips masked values (****) since they can't be compared.

  2. Event 15 handler (Z-Wave JS only, _async_handle_duplicate_code()): Reactive safety net for cases where the pre-flight check can't detect the duplicate (e.g., masked codes from unmanaged slots).

Both paths disable the slot and create a persistent notification.

Sync Attempt Tracking

The sync mechanism tracks consecutive successful SET attempts (provider call did not raise) that fail to resolve the out-of-sync state. If MAX_SYNC_ATTEMPTS are reached within SYNC_ATTEMPT_WINDOW, the lock is assumed to be rejecting the PIN. The slot is disabled and the user is notified.

Only SET operations are tracked — clears always proceed and are not counted. LockDisconnected exceptions are transient and use the separate retry mechanism with RETRY_DELAY. The tracker resets when the slot is disabled or when the code is successfully synced.

Exception Hierarchy for Code Rejection

  • CodeRejectedError — base: the lock won't accept this PIN (any reason)
  • DuplicateCodeError(CodeRejectedError) — specific: PIN duplicates another slot

The pre-flight duplicate check raises DuplicateCodeError. The sync path catches the parent CodeRejectedError so any rejection reason results in disable + notify.

Base Provider (BaseLock)

The @final methods async_internal_set_usercode and async_internal_clear_usercode are the single entry point for all lock operations. They enforce cross-cutting concerns in this order:

  1. Integration connectivity check — verify integration is connected
  2. Device availability check — verify physical device is responsive
  3. Acquire operation lock — serialize operations via asyncio.Lock
  4. Pre-execute hook (set only) — duplicate code check runs inside the lock
  5. Rate limit delay — minimum delay between operations
  6. Provider call — delegate to subclass async_set_usercode / async_clear_usercode
  7. Coordinator refresh — update state (skipped for push-based providers)

Helper methods available to all providers:

  • is_masked_or_empty(code) — detect **** masked or empty codes
  • is_slot_managed(code_slot) — check if any LCM config entry manages this slot

The source parameter ("sync" or "direct") indicates whether the call came from the sync path (binary sensor) or a user action (websocket). Currently informational; future use for differentiated error handling policies.

Rate Limiting

All lock operations are serialized via asyncio.Lock per provider instance, with a 2-second minimum delay between operations. This prevents overwhelming the lock's radio and Z-Wave mesh.

WebSocket Subscriptions

WebSocket subscriptions for slot card data re-resolve entity IDs dynamically on each update. This handles entities that are created after the subscription is established (for example, during initial config setup). The resolution uses lightweight entity registry lookups.

Lovelace Dashboard Updates

When structural config changes occur (slots or locks added/removed), LCM fires lovelace_updated events for each registered dashboard. This triggers the "Configuration changed" toast in the Home Assistant frontend, prompting users to refresh so the strategy re-generates cards.

Frontend

The frontend is built with TypeScript 6.0 and provides Lovelace strategies (dashboard, view, and section level) and custom cards (lcm-slot, lcm-lock-codes) for managing PINs.

Clone this wiki locally