I have two Z-Wave deadbolts on the house — one on the front door, one on the door from the garage into the laundry room. Both run PIN code management through Keymaster, the long-standing HACS integration for Z-Wave lock code slot management. They’ve been running like that for over a year. They mostly work.
But if I were starting over today, I’d use Lock Code Manager instead, and the Keymaster install I have now is on borrowed time. This post is the comparison, the actual notification automation I run, and the lessons from the most painful Keymaster bug I’ve hit.
What lock code management actually means
Both of my deadbolts are Z-Wave with built-in keypads. They store up to ~30 PIN codes locally on the lock itself; you punch in a code, the lock opens. The codes are stored in numbered “slots.” Slot 1 might be my code, slot 2 my wife’s, slot 3 the dog walker’s, and so on.
Out of the box, Home Assistant’s Z-Wave JS integration only exposes the lock as a binary entity (lock.frontdoor — locked or unlocked) and a few diagnostic sensors. It does not give you a friendly way to manage which PINs are in which slots, restrict slots to certain dates and times, or get a notification telling you who just unlocked the door.
That’s the gap Keymaster (and Lock Code Manager) fills. They sit on top of Z-Wave JS and add:
- A slot-management UI (assign a name and a PIN to slot 1, 2, 3, etc.)
- Time/day restrictions per slot (“dog walker’s PIN only works Mon-Fri 9-11 AM”)
- Slot-aware unlock events (so you can see which slot was used)
- Auto-generated Lovelace dashboards for managing all of this
Useful for a four-person household with periodic guests, dog walkers, cleaners, contractors. Not strictly necessary if you only have two PINs and never change them.
The two integrations
There are essentially two HACS integrations doing this job in 2026:
Keymaster (FutureTense/keymaster) — the older, more entrenched option. Has been the de-facto answer in HA forums for years. Mature, lots of community guides, well-known.
Lock Code Manager (raman325/lock_code_manager) — the newer, cleaner alternative. Designed explicitly as a modern replacement, supports Z-Wave + Matter + a wider lock list (including Schlage models that Keymaster historically struggled with), produces fewer entities per slot, and has lighter-weight automation requirements.
I’m running Keymaster because it’s what I installed in 2024 when the choice was effectively just Keymaster. If I were starting clean today, the choice is obvious — but I’m not, and migrating is non-trivial because the entity names change.
The Keymaster issues that are pushing me to migrate
Keymaster works for me most of the time. But I’ve hit three specific failure modes in the past year:
The 2025.8 update broke it. Z-Wave JS in HA 2025.8 made changes to how lock command-class events are surfaced, and Keymaster’s PIN-add flow got jammed: I’d add a PIN to a slot via the Lovelace dashboard, the slot would go into “Adding” status, and stay there indefinitely. The lock never received the new code. Recovery required manually removing the integration entry, restarting HA, re-adding it, and re-syncing every existing slot. Took an evening.
Disconnected state errors. Even after the 2025.8 issue settled, Keymaster periodically reports a Z-Wave node as “Disconnected” when the actual node is fine and Z-Wave JS is communicating with it normally. The fix is to delete and re-add the integration entry for the affected lock, but it shouldn’t need a fix — Keymaster’s connection state model and Z-Wave JS’s are slightly out of sync.
Entity sprawl. Each lock with 10 active slots produces 60+ entities — input_text per slot for PIN, input_text for name, input_datetime for start/end, input_boolean for enabled, etc. With two locks and 30 slots, the entity registry gets crowded. None of these entities have a keymaster_ prefix, so they’re scattered alphabetically with everything else and clutter the entity picker. Lock Code Manager produces dramatically fewer entities — closer to 10 per slot — and is built around a slot-management service rather than a pile of input helpers.
The orphaned-lock recovery. Once, my front-door lock physically lost Z-Wave network association (battery died at the wrong moment, came back up not paired). Re-pairing it through Z-Wave JS worked, but Keymaster still had a config entry pointing to the old ghost device. Disabling and re-enabling the Keymaster config entry got it linked to the re-paired lock. Not fun, took half an hour to figure out.
None of these are dealbreakers individually. Together they’re enough to make me wish I’d started on Lock Code Manager.
What I run today: slot-aware unlock notifications
Whatever integration you use, the most useful automation in my Keymaster setup is “tell me who just unlocked the door, by name.” Z-Wave deadbolts emit an alarm_type=19 event when unlocked via keypad, with alarm_level equal to the slot number used. Keymaster lets you map slot numbers to names, so the automation becomes:
- id: 'lock_keypad_unlock_notify_charles'
alias: Lock Keypad Unlock Notification (Charles Only)
description: 'Notify Charles when any lock is unlocked via keypad with who did it'
triggers:
- entity_id: sensor.frontdoor_alarmtype
trigger: state
to: '19'
id: frontdoor
- entity_id: sensor.deadbolt_garagedoor_alarmtype
trigger: state
to: '19'
id: garagedoor
conditions: []
actions:
- variables:
lock_name: >-
{% if trigger.id == 'frontdoor' %}Front Door
{% else %}Garage Door{% endif %}
slot: >-
{% if trigger.id == 'frontdoor' %}
{{ states('sensor.frontdoor_alarmlevel') | int(0) }}
{% else %}
{{ states('sensor.deadbolt_garagedoor_alarmlevel') | int(0) }}
{% endif %}
slot_name_entity: >-
{% if trigger.id == 'frontdoor' %}
text.frontdoor_code_slot_{{ states('sensor.frontdoor_alarmlevel') | int(0) }}_name
{% else %}
text.garagedoor_code_slot_{{ states('sensor.deadbolt_garagedoor_alarmlevel') | int(0) }}_name
{% endif %}
person_name: >-
{{ states(slot_name_entity) if states(slot_name_entity) not in ['unknown', 'unavailable', ''] else 'Unknown (Slot ' ~ slot ~ ')' }}
- action: notify.charles
data:
title: "🔓 {{ lock_name }} Unlocked"
message: "{{ person_name }} unlocked the {{ lock_name | lower }} via keypad"
data:
tag: "lock-unlock-{{ trigger.id }}"
Walking through what this does:
- Fires on either lock’s
alarmtypesensor changing to 19 (Z-Wave alarm type for “keypad unlock”). Both triggers tagged withidso the action knows which lock. - Builds three variables: a friendly lock name, the slot number from the
alarmlevelsensor, and the entity ID of the slot’s name field —text.frontdoor_code_slot_4_name, for example. - Resolves the person name by looking up that entity. If the slot has no name set (state is unavailable / unknown / empty), falls back to “Unknown (Slot N)” so the notification still has useful info.
- Sends a tagged notification. The tag means a second unlock at the same door replaces the first notification rather than stacking.
End result: when the cleaner unlocks the front door, my phone says ”🔓 Front Door Unlocked: Cleaner unlocked the front door via keypad”. When the dog walker unlocks the garage, the notification says her name. If someone tries a slot that’s been emptied, I get “Unknown (Slot 7)” — useful as a security signal (someone tried a slot we don’t recognize).
This pattern works the same way under Lock Code Manager. The entity names are different (text.frontdoor_lcm_slot_4_name vs text.frontdoor_code_slot_4_name), but the alarm-type-19 event is the same Z-Wave-level signal regardless of which integration is reading it. If I migrate, I’ll just s/code_slot/lcm_slot/ in the template.
Cross-lock slot syncing
A small cute thing I have is automations that copy time-restriction settings between slots on the two locks. If I set the front-door slot 4 (cleaner) to start Wednesdays at 9 AM, an automation copies that start time to garage-door slot 4. Means I only edit settings once. Looks like:
- id: '1748887396910'
alias: Keymaster Copy Frontdoor Wed Start Date to Garagedoor
description: 'Sync Wednesday start date from frontdoor to garagedoor slot 4'
triggers:
- entity_id: input_datetime.wed_start_date_frontdoor_4
trigger: state
conditions:
- condition: state
entity_id: input_boolean.override_parent_garagedoor_4
state: 'off'
actions:
- target:
entity_id: input_datetime.wed_start_date_garagedoor_4
data:
time: '{{ states.input_datetime.wed_start_date_frontdoor_4.state }}'
action: input_datetime.set_datetime
The override_parent boolean is a per-slot override — if it’s on, the sync skips, and the garage-door slot keeps its independent value. Lets me hand the cleaner garage-only access during a one-off scheduling change.
This is the kind of thing that’s possible because Keymaster’s slots are exposed as discrete input_datetime / input_boolean / text helpers — you can wire them together with normal HA automations. Lock Code Manager has a more service-call-driven model, which is cleaner architecturally but means cross-slot syncing requires a slightly different pattern.
What I’d build now
Three pieces of advice based on a year of running this:
-
Use Lock Code Manager unless you have a specific reason not to. The 2025.8 breakage and the entity sprawl are real, and the maintenance trajectory of Lock Code Manager is better. The only reason to pick Keymaster in 2026 is if you have a lock model that LCM doesn’t support — and that list is short.
-
Don’t put a keypad-unlock notification behind a
for:debounce. I tried that early on, thinking it would prevent duplicate fires from a single press. It actually loses the first notification (the one you care about) on a quick unlock-then-relock; the alarm type goes back to its default before thefor:window expires. Use the notification tag instead — let HA fire the notification and let the OS dedupe. -
Keep PIN slots short and rotate them. A 4-digit PIN is fine for short-term access (cleaner, one-off contractor) but it’s a tiny keyspace — 10000 codes — and a determined attacker with physical access can brute-force it. Time-restricted slots help: a PIN that only works Tuesdays 9-11 AM has 99% less attack surface than a 24/7 PIN. Rotate any PINs given to outside service providers when the relationship ends.
What’s still missing for me
The piece I haven’t built yet, and probably should: a “lock didn’t lock” alarm. The lock has an auto-relock-after-5-minutes setting, but if for some reason the bolt doesn’t engage (warped door, cold weather affecting alignment), I want a notification rather than just trusting the lock’s silence. The Z-Wave lock entity does report whether the bolt is jammed, so it’s a one-automation job; I just haven’t done it.
If you want to skip the rest of this post: install Lock Code Manager, not Keymaster. Build slot-aware unlock notifications with a tag-based dedupe rather than a for: debounce. Cycle PINs.