Describe the bug
Four defects in the Zigbee2MQTT lock provider chain, found while setting up a Yale Assure lock. They are sequential: each one hides the next, so the first three produce no visible symptom beyond "keypad usage is never recorded".
I have patched all four locally and keypad events now populate Last Used correctly. Happy to raise a PR.
Bug 1 — base_topic subscribes to the IEEE address, not the friendly name
providers/zigbee2mqtt.py:
python
Extract the original Z2M friendly name from device identifiers to support device renaming
for domain, identifier in device_entry.identifiers:
if domain == MQTT_DOMAIN and identifier.startswith("zigbee2mqtt_"):
friendly_name = identifier[len("zigbee2mqtt_") :]
if friendly_name:
return f"zigbee2mqtt/{friendly_name}"
Zigbee2MQTT always sets identifiers to zigbee2mqtt_, never the friendly name. Mine is zigbee2mqtt_0x000d6f001933df17 with friendly name Door Lock, so Keymaster subscribes to zigbee2mqtt/0x000d6f001933df17 — a topic Z2M never publishes to.
This only works out of the box for a device that has never been renamed in Z2M (default friendly_name == IEEE address). The README's own expose_pin example uses friendly_name: front_door_lock, which hits this.
Writes are unaffected because Z2M accepts the IEEE address on /set and /get, so the integration looks healthy — codes write fine — while Timeout querying slot N repeats every ~71s forever and the users payload is never parsed.
Suggested fix: prefer device_entry.name (populated by HA from the Z2M friendly name via discovery) and keep the identifier suffix only as a fallback.
Bug 2 — keypad action vocabulary too narrow
_async_handle_action matches only keypad_unlock / keypad_lock. My Yale converter emits action: "unlock" with action_source_name: "keypad" and action_user: 1. Neither branch matches, so no event fires even once the topic is correct.
action_source_name looks like the reliable discriminator, since unlock also appears for RF/app-initiated operations.
Bug 3 — entity state overrides an unambiguous event label
coordinator.py, _handle_provider_lock_event:
python
if state_changed:
inferred_action = new_state
elif "unlock" in label_lower:
inferred_action = LockState.UNLOCKED
Z2M delivers the keypad notification in the same payload as the pre-operation lock_state, so at callback time the lock entity still reads locked while the label says Unlocked via Keypad. The state_changed branch wins and routes the event to _lock_locked, so Last Used never fires.
The comment above this block says it is meant to handle "providers that fire events before the entity state updates" — but the state_changed check is evaluated first and defeats exactly that case.
Bug 4 — kmlock.lock_state never updates for push providers
_handle_lock_state_change skips _lock_locked / _lock_unlocked entirely when supports_push_updates is true ("For push providers this listener is autolock-only"), so it never writes kmlock.lock_state. That state is only maintained inside those two methods, reachable only via the provider lock-event path.
But the provider event path requires isinstance(slot_num, int), and Z2M's one-touch/manual lock publishes action_user: null. So a physical lock produces no provider event, kmlock.lock_state is stuck at UNLOCKED indefinitely, and every subsequent keypad unlock is discarded by the guard at the top of _lock_unlocked:
python
if kmlock.lock_state == LockState.UNLOCKED:
... # supersede only when prior_slot == 0
return
Each path assumes the other maintains the state. Suggested fix: sync kmlock.lock_state in _handle_lock_state_change for push providers too (evaluating the autolock condition before the assignment, since it tests the old value).
Bug 5 (separate, user-visible) — reloading the config entry deletes the Lovelace strategy resource
async_setup registers /keymaster_files/keymaster.js, but async_unload_entry calls async_cleanup_strategy_resource when the last entry unloads. A config-entry reload is an unload followed by async_setup_entry — which never re-registers. So clicking Reload on the integration page permanently removes the resource until HA is restarted, and every dashboard using the strategy shows Timeout waiting for strategy element ll-strategy-section-keymaster to be registered.
Suggested fix: skip the cleanup on reload, or re-register in async_setup_entry.
Home Assistant version
2026.8.0b3
Keymaster version
v0.5.3
Home Assistant installation type
Home Assistant OS
Lock provider
zigbee2mqtt
Lock make and model
Yale Assure Lock (YRD226HA2619)
Steps to reproduce
Pair a Zigbee lock in Zigbee2MQTT with expose_pin: true, and give it a friendly_name that is not its IEEE address (e.g. Door Lock).
Configure it in Keymaster with the zigbee2mqtt provider.
Write a PIN to slot 1 — succeeds, so setup appears healthy.
HA log: [Zigbee2MQTTProvider] Timeout querying slot 1 repeats indefinitely (bug 1).
Fix the topic; keypad unlock still fires nothing (bug 2).
Fix the action matching; the event now reaches the coordinator but is routed to _lock_locked (bug 3).
Fix the inference; the event reaches _lock_unlocked and is dropped silently by the lock_state == UNLOCKED guard (bug 4).
Separately: reload the Keymaster config entry, then load any dashboard using the keymaster section strategy (bug 5).
Expected behavior
Keymaster subscribes to zigbee2mqtt/<friendly_name>, populates its usercode cache from the users payload, tracks lock/unlock state correctly for push providers, and fires the keypad-unlock event so event._code_slot_N_last_used updates. Reloading the config entry leaves the Lovelace strategy resource registered.
Logs
Bug 1 — HA, repeating every ~71 seconds:
[Zigbee2MQTTProvider] Timeout querying slot 1
[Zigbee2MQTTProvider] Error querying slot 1: Timeout querying slot 1
Z2M receives the get on the IEEE topic (Z2M accepts it):
debug: z2m:mqtt: Received MQTT message on 'zigbee2mqtt/0x000d6f001933df17/get' with data '{"pin_code": {"user": 1}}'
debug: zh:controller:endpoint: ZCL command 0x000d6f001933df17/1 closuresDoorLock.getPinCode({"userid":1}, ...)
...but publishes the response to the friendly-name topic, which Keymaster is not subscribed to:
info: z2m:mqtt: MQTT publish: topic 'zigbee2mqtt/Door Lock', payload '{"action":null,"action_source_name":null,"action_user":null,"battery":58,"lock_state":"unlocked","state":"UNLOCK","users":{"1":{"pin_code":"REDACTED","status":"enabled"}, ...}}'
Bug 2 — keypad unlock as published by Z2M:
topic 'zigbee2mqtt/Door Lock' -> {"action":"unlock","action_source":0,"action_source_name":"keypad","action_user":1, ...}
Bug 3 — after fixing 1 and 2, with keymaster at DEBUG:
[handle_lock_event_from_provider] Door Lock: event_label: Unlocked via Keypad, new_state: locked, inferred_action: locked, code_slot_num: 1, action_code: 1
[lock_locked] Door Lock: Running. source: event, event_label: Unlocked via Keypad, action_code: 1
Bug 4 — after fixing 3, inference is correct but the event is dropped with no further log line:
[handle_lock_state_change] Door Lock: old_state: unlocked, new_state: locked <- no [lock_locked] follows
[handle_lock_event_from_provider] Door Lock: event_label: Unlocked via Keypad, new_state: locked, inferred_action: unlocked, code_slot_num: 1, action_code: 1
(no [lock_unlocked] line, no "Throttled", no "Superseding")
After all four patches:
event.door_lock_code_slot_1_last_used = 2026-08-02T00:31:37.219+00:00
attributes: {"code_slot_num": 1, "code_slot_name": "Family", "event_type": "unlocked", "lockname": "Door Lock"}
Screenshots
No response
Additional context
Zigbee2MQTT 2.13.0-1 (HA add-on). HA device registry for the lock: identifiers {('mqtt', 'zigbee2mqtt_0x000d6f001933df17')}, device name Door Lock, model Assure lock / YRD226HA2619.
Bugs 1-4 are sequential — fixing any one alone produces no visible change, which is likely why this has gone unreported. Bug 5 is independent and probably the most user-visible of the set, since Reload is offered on every integration page.
Describe the bug
Four defects in the Zigbee2MQTT lock provider chain, found while setting up a Yale Assure lock. They are sequential: each one hides the next, so the first three produce no visible symptom beyond "keypad usage is never recorded".
I have patched all four locally and keypad events now populate Last Used correctly. Happy to raise a PR.
Bug 1 — base_topic subscribes to the IEEE address, not the friendly name
providers/zigbee2mqtt.py:
python
Extract the original Z2M friendly name from device identifiers to support device renaming
for domain, identifier in device_entry.identifiers:
if domain == MQTT_DOMAIN and identifier.startswith("zigbee2mqtt_"):
friendly_name = identifier[len("zigbee2mqtt_") :]
if friendly_name:
return f"zigbee2mqtt/{friendly_name}"
Zigbee2MQTT always sets identifiers to zigbee2mqtt_, never the friendly name. Mine is zigbee2mqtt_0x000d6f001933df17 with friendly name Door Lock, so Keymaster subscribes to zigbee2mqtt/0x000d6f001933df17 — a topic Z2M never publishes to.
This only works out of the box for a device that has never been renamed in Z2M (default friendly_name == IEEE address). The README's own expose_pin example uses friendly_name: front_door_lock, which hits this.
Writes are unaffected because Z2M accepts the IEEE address on /set and /get, so the integration looks healthy — codes write fine — while Timeout querying slot N repeats every ~71s forever and the users payload is never parsed.
Suggested fix: prefer device_entry.name (populated by HA from the Z2M friendly name via discovery) and keep the identifier suffix only as a fallback.
Bug 2 — keypad action vocabulary too narrow
_async_handle_action matches only keypad_unlock / keypad_lock. My Yale converter emits action: "unlock" with action_source_name: "keypad" and action_user: 1. Neither branch matches, so no event fires even once the topic is correct.
action_source_name looks like the reliable discriminator, since unlock also appears for RF/app-initiated operations.
Bug 3 — entity state overrides an unambiguous event label
coordinator.py, _handle_provider_lock_event:
python
if state_changed:
inferred_action = new_state
elif "unlock" in label_lower:
inferred_action = LockState.UNLOCKED
Z2M delivers the keypad notification in the same payload as the pre-operation lock_state, so at callback time the lock entity still reads locked while the label says Unlocked via Keypad. The state_changed branch wins and routes the event to _lock_locked, so Last Used never fires.
The comment above this block says it is meant to handle "providers that fire events before the entity state updates" — but the state_changed check is evaluated first and defeats exactly that case.
Bug 4 — kmlock.lock_state never updates for push providers
_handle_lock_state_change skips _lock_locked / _lock_unlocked entirely when supports_push_updates is true ("For push providers this listener is autolock-only"), so it never writes kmlock.lock_state. That state is only maintained inside those two methods, reachable only via the provider lock-event path.
But the provider event path requires isinstance(slot_num, int), and Z2M's one-touch/manual lock publishes action_user: null. So a physical lock produces no provider event, kmlock.lock_state is stuck at UNLOCKED indefinitely, and every subsequent keypad unlock is discarded by the guard at the top of _lock_unlocked:
python
if kmlock.lock_state == LockState.UNLOCKED:
... # supersede only when prior_slot == 0
return
Each path assumes the other maintains the state. Suggested fix: sync kmlock.lock_state in _handle_lock_state_change for push providers too (evaluating the autolock condition before the assignment, since it tests the old value).
Bug 5 (separate, user-visible) — reloading the config entry deletes the Lovelace strategy resource
async_setup registers /keymaster_files/keymaster.js, but async_unload_entry calls async_cleanup_strategy_resource when the last entry unloads. A config-entry reload is an unload followed by async_setup_entry — which never re-registers. So clicking Reload on the integration page permanently removes the resource until HA is restarted, and every dashboard using the strategy shows Timeout waiting for strategy element ll-strategy-section-keymaster to be registered.
Suggested fix: skip the cleanup on reload, or re-register in async_setup_entry.
Home Assistant version
2026.8.0b3
Keymaster version
v0.5.3
Home Assistant installation type
Home Assistant OS
Lock provider
zigbee2mqtt
Lock make and model
Yale Assure Lock (YRD226HA2619)
Steps to reproduce
Pair a Zigbee lock in Zigbee2MQTT with expose_pin: true, and give it a friendly_name that is not its IEEE address (e.g. Door Lock).
Configure it in Keymaster with the zigbee2mqtt provider.
Write a PIN to slot 1 — succeeds, so setup appears healthy.
HA log: [Zigbee2MQTTProvider] Timeout querying slot 1 repeats indefinitely (bug 1).
Fix the topic; keypad unlock still fires nothing (bug 2).
Fix the action matching; the event now reaches the coordinator but is routed to _lock_locked (bug 3).
Fix the inference; the event reaches _lock_unlocked and is dropped silently by the lock_state == UNLOCKED guard (bug 4).
Separately: reload the Keymaster config entry, then load any dashboard using the keymaster section strategy (bug 5).
Expected behavior
Keymaster subscribes to zigbee2mqtt/<friendly_name>, populates its usercode cache from the users payload, tracks lock/unlock state correctly for push providers, and fires the keypad-unlock event so event._code_slot_N_last_used updates. Reloading the config entry leaves the Lovelace strategy resource registered.
Logs
Screenshots
No response
Additional context
Zigbee2MQTT 2.13.0-1 (HA add-on). HA device registry for the lock: identifiers {('mqtt', 'zigbee2mqtt_0x000d6f001933df17')}, device name Door Lock, model Assure lock / YRD226HA2619.
Bugs 1-4 are sequential — fixing any one alone produces no visible change, which is likely why this has gone unreported. Bug 5 is independent and probably the most user-visible of the set, since Reload is offered on every integration page.