Skip to content

Matter Lock: stale lockAlarm state can remain after profile migration #2946

@ldeora

Description

@ldeora

Summary

The new-matter-lock sub-driver can leave a stale lockAlarm.alarm value in device state when a Matter lock starts on a fallback/static profile that does not support lockAlarm, and later migrates to a modular profile that does support it.

In the observed case, a Nuki Smart Lock Ultra still showed:

"lockAlarm": {
  "supportedAlarmValues": {
    "value": [
      "unableToLockTheDoor"
    ]
  },
  "alarm": {
    "value": "unableToLockTheDoor"
  }
}

even though the lock was online, working normally, and reporting lock = locked.

The stale alarm value had an old timestamp and appeared to be persisted state that was not cleared after the profile changed.

Tested device

This was observed with a real Nuki Smart Lock Ultra paired to SmartThings via Matter.

The same device also demonstrates the modular profile migration path discussed separately: after the DoorLock FeatureMap is processed, the driver can migrate the device to:

lock-modular-embedded-unlatch

That profile supports lockAlarm.

Observed behavior in logs

When the device is first handled, it starts from a fallback/static profile that does not support lockAlarm.

During the added lifecycle event, the driver attempts to emit a lockAlarm event:

received lifecycle event: added
Attempted to generate event for <device-id>.main but it does not support capability lockAlarm

This means the initial alarm.clear() event cannot be applied because the active profile does not yet include the lockAlarm capability.

Later, after the FeatureMap is read and the device migrates to the modular profile, the device can support lockAlarm. In the working reprofiling path, the log shows:

Updating device profile to lock-modular-embedded-unlatch. Enabling the optional capabilities [battery] on component 'main'

and then after infoChanged:

received lifecycle event: infoChanged
emitting event: {"attribute_id":"alarm","capability_id":"lockAlarm","component_id":"main","state":{"value":"clear"},"state_change":true}

This confirms that alarm.clear() can only be emitted successfully after the profile has changed to one that includes lockAlarm.

Current issue

The driver currently has lock-alarm initialization logic in multiple lifecycle paths, but it is not consistently guarded by whether the current profile actually supports lockAlarm.

As a result:

  1. alarm.clear() may be emitted too early, while the fallback profile does not support lockAlarm.
  2. That event is rejected/ignored by the platform.
  3. After the profile migration, the driver may not reliably emit alarm.clear() again in all relevant lifecycle paths.
  4. A stale persisted alarm such as unableToLockTheDoor can remain visible in component status even though it is no longer current.

This is especially confusing because supportedAlarmValues and the current alarm state are different concepts:

  • supportedAlarmValues is capability metadata.
  • alarm.clear() is current state cleanup.

The driver should not only emit alarm.clear() when supportedAlarmValues is missing. A stale current alarm can exist even when supportedAlarmValues is already present.

Expected behavior

The driver should initialize/clear lockAlarm only when the active profile supports lockAlarm, and should do so after lifecycle events where the profile may have changed.

Expected behavior:

  1. If the current profile does not support lockAlarm, do not attempt to emit lockAlarm events.
  2. Once the profile supports lockAlarm, emit alarm.clear() to remove stale alarm state.
  3. Emit supportedAlarmValues({"unableToLockTheDoor"}) only if the metadata is missing.
  4. Continue to handle real DoorLockAlarm events normally.

Suggested fix

Add a small helper that initializes the lockAlarm capability only when the current profile supports it:

local function initialize_lock_alarm(device)
  if not device:supports_capability_by_id(capabilities.lockAlarm.ID) then
    return
  end

  device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true}))
  if device:get_latest_state("main", capabilities.lockAlarm.ID, capabilities.lockAlarm.supportedAlarmValues.NAME) == nil then
    device:emit_event(capabilities.lockAlarm.supportedAlarmValues(
      {"unableToLockTheDoor"},
      {visibility = {displayed = false}}
    )) -- lockJammed is mandatory
  end
end

Then use that helper from lifecycle paths where the current profile may already support, or may have just gained, lockAlarm support:

local function device_added(driver, device)
  initialize_lock_alarm(device)
end

local function info_changed(driver, device, event, args)
  ...
  initialize_lock_alarm(device)
end

local function do_configure(driver, device)
  match_profile(driver, device, false)
  device.thread:call_with_delay(5, function()
    initialize_lock_alarm(device)
  end)
end

local function driver_switched(driver, device)
  match_profile(driver, device, false)
  initialize_lock_alarm(device)
end

This keeps the behavior conservative:

  • It does not emit unsupported lockAlarm events while the fallback profile is active.
  • It clears stale alarm state once lockAlarm is actually available.
  • It does not change DoorLockAlarm event handling.
  • It does not change supported alarm values, except to initialize them if missing.

Why this matters

A stale unableToLockTheDoor alarm can make a normally working lock appear to be in an error state. For door locks, this is particularly undesirable because users may interpret the old alarm as a current lock failure.

The issue is not that the lock is currently unable to lock. The issue is that an old persisted alarm state is not reliably cleared after the profile/capability set changes.

@hcarter-775 @ctowns Since this is somewhat of a timing issue and I can only observe it on my device, this should be replicated with other real devices.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions