Added Door State to Matter lock driver

The current stock Matter lock driver doesn’t support the door state, so I just added it. I’m using the contactSensor capability until there’s a better alternative in the production capabilities. Only the DOOR_CLOSED state is considered as closed, every other state is considered as open.

This is what it looks like:

Driver can be found in this channel: SmartThings. Add a little smartness to your things.

And here’s the patch if you want to apply it yourself:

diff '--color=auto' -Nur matter-lock.orig/config.yml matter-lock/config.yml
--- matter-lock.orig/config.yml 2025-04-25 11:49:48.054721221 +0200
+++ matter-lock/config.yml      2025-04-17 12:54:09.365940200 +0200
@@ -1,5 +1,5 @@
-name: 'Matter Lock'
-packageKey: 'matter-lock'
+name: 'Matter Lock (AR)'
+packageKey: 'matter-lock-ar'
 permissions:
   matter: {}
 description: "SmartThings driver for Matter lock devices"
diff '--color=auto' -Nur matter-lock.orig/profiles/lock-unlatch-contact-battery.yml matter-lock/profiles/lock-unlatch-contact-battery.yml
--- matter-lock.orig/profiles/lock-unlatch-contact-battery.yml  1970-01-01 01:00:00.000000000 +0100
+++ matter-lock/profiles/lock-unlatch-contact-battery.yml       2025-04-17 12:53:52.825637118 +0200
@@ -0,0 +1,131 @@
+name: lock-unlatch-contact-battery
+components:
+- id: main
+  capabilities:
+  - id: lock
+    version: 1
+    config:
+      values:
+      - key: "lock.value"
+        enabledValues:
+        - locked
+        - unlocked
+        - unlatched
+        - not fully locked
+  - id: lockAlarm
+    version: 1
+  - id: remoteControlStatus
+    version: 1
+  - id: contactSensor
+    version: 1
+  - id: battery
+    version: 1
+  - id: firmwareUpdate
+    version: 1
+  - id: refresh
+    version: 1
+  categories:
+  - name: SmartLock
+deviceConfig:
+  dashboard:
+    states:
+    - component: main
+      capability: lock
+      version: 1
+    - component: main
+      capability: contactSensor
+      version: 1
+    actions:
+    - component: main
+      capability: lock
+      version: 1
+      visibleCondition: {
+        "capability": "lock",
+        "version": "1",
+        "component": "main",
+        "value": "lock.value",
+        "operator": "DOES_NOT_EQUAL",
+        "operand": "unlatched"
+      }
+  detailView:
+  - component: main
+    capability: lock
+    version: 1
+    values:
+    - key: lock.value
+      alternatives:
+      - key: locked
+        type: inactive
+        value: '{{i18n.attributes.lock.i18n.value.locked.label}}'
+      - key: unlocked
+        value: '{{i18n.attributes.lock.i18n.value.unlocked.label}}'
+      - key: unlatched
+        value: '{{i18n.attributes.lock.i18n.value.unlatched.label}}'
+      - key: not fully locked
+        value: '{{i18n.attributes.lock.i18n.value.not fully locked.label}}'
+    patch:
+    - op: add
+      path: /1
+      value:
+        capability: lock
+        version: 1
+        component: main
+        label: '{{i18n.commands.unlatch.label}}'
+        displayType: pushButton
+        pushButton:
+          command: unlatch
+  - component: main
+    capability: lockAlarm
+    version: 1
+  - component: main
+    capability: remoteControlStatus
+    version: 1
+  - component: main
+    capability: contactSensor
+    version: 1
+  - component: main
+    capability: battery
+    version: 1
+  automation:
+    conditions:
+    - component: main
+      capability: lock
+      version: 1
+      values:
+      - key: lock.value
+        alternatives:
+        - key: locked
+          type: inactive
+          value: '{{i18n.attributes.lock.i18n.value.locked.label}}'
+        - key: unlocked
+          value: '{{i18n.attributes.lock.i18n.value.unlocked.label}}'
+        - key: unlatched
+          value: '{{i18n.attributes.lock.i18n.value.unlatched.label}}'
+        - key: not fully locked
+          value: '{{i18n.attributes.lock.i18n.value.not fully locked.label}}'
+    - component: main
+      capability: lockAlarm
+      version: 1
+    - component: main
+      capability: remoteControlStatus
+      version: 1
+    - component: main
+      capability: contactSensor
+      version: 1
+    - component: main
+      capability: battery
+      version: 1
+    actions:
+    - component: main
+      capability: lock
+      version: 1
+      values:
+      - key: '{{enumCommands}}'
+        alternatives:
+        - key: lock
+          type: inactive
+          value: '{{i18n.commands.lock.label}}'
+        - key: unlock
+          value: '{{i18n.commands.unlock.label}}'
+        - key: unlatch
+          value: '{{i18n.commands.unlatch.label}}'
diff '--color=auto' -Nur matter-lock.orig/src/new-matter-lock/init.lua matter-lock/src/new-matter-lock/init.lua
--- matter-lock.orig/src/new-matter-lock/init.lua       2025-04-25 11:49:48.063722631 +0200
+++ matter-lock/src/new-matter-lock/init.lua    2025-04-25 12:38:49.783969510 +0200
@@ -71,6 +71,9 @@
     DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser,
     DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser
   },
+  [capabilities.contactSensor.ID] = {
+    DoorLock.attributes.DoorState
+  },
   [capabilities.battery.ID] = {
     PowerSource.attributes.BatPercentRemaining
   },
@@ -150,6 +153,7 @@
   local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES})
   local year_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES})
   local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT})
+  local doorstate_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.DOOR_STATE})
   local battery_eps = device:get_endpoints(PowerSource.ID, {feature_bitmap = PowerSource.types.PowerSourceFeature.BATTERY})
 
   local profile_name = "lock"
@@ -168,6 +172,10 @@
   else
     device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}}))
   end
+  if #doorstate_eps > 0 then
+    profile_name = profile_name .. "-contact"
+    device:add_subscribed_attribute(DoorLock.attributes.DoorState)
+  end
   if #battery_eps > 0 then
     device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true})
     local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {})
@@ -238,6 +246,27 @@
   end)
 end
 
+-- NEW: DoorState handler
+local function door_state_handler(driver, device, ib, response)
+  local DoorState = DoorLock.types.DoorStateEnum
+  local attr = capabilities.contactSensor.contact
+
+  local DOOR_STATE_MAP = {
+    [DoorState.DOOR_OPEN] = attr.open(),
+    [DoorState.DOOR_CLOSED] = attr.closed(),
+    [DoorState.DOOR_JAMMED] = attr.open(), -- Optional mapping
+    [DoorState.DOOR_FORCED_OPEN] = attr.open(), -- Optional mapping
+    [DoorState.DOOR_UNSPECIFIED_ERROR] = attr.open(), -- Optional mapping
+    [DoorState.DOOR_AJAR] = attr.open(), -- Optional mapping
+  }
+
+  if ib.data.value ~= nil then
+    device:emit_event(DOOR_STATE_MAP[ib.data.value])
+  else
+    device.log.warn("Door State is nil")
+  end
+end
+  
 ---------------------
 -- Operating Modes --
 ---------------------
@@ -1743,6 +1772,7 @@
     attr = {
       [DoorLock.ID] = {
         [DoorLock.attributes.LockState.ID] = lock_state_handler,
+        [DoorLock.attributes.DoorState.ID] = door_state_handler,
         [DoorLock.attributes.OperatingMode.ID] = operating_modes_handler,
         [DoorLock.attributes.NumberOfTotalUsersSupported.ID] = total_users_supported_handler,
         [DoorLock.attributes.NumberOfPINUsersSupported.ID] = pin_users_supported_handler,
@@ -1809,6 +1839,7 @@
     capabilities.lockUsers,
     capabilities.lockCredentials,
     capabilities.lockSchedules,
+    capabilities.contactSensor,
     capabilities.battery,
     capabilities.batteryLevel
   },

TODO: Custom capability, GitHub PR. :face_with_spiral_eyes:

6 Likes

Hi, @Andreas_Roedl
I found this in the Matter Cluster Specification. Is that the “door state” you’re discussing here?

In the case of your device, would that be connected through a manufacturer-specific or a generic fingerprint?
I’m asking because I’m not sure if every device supports this cluster and if yours is part of the certified ones I can present the case for the engineering team.
Having the model and brand would also help to have a better reference.

Yes, that’s the correct spec. It’s a Nuki Ultra lock and so far the only Matter Lock that has this Matter feature. I’m in touch with someone from Aqara to support the DoorState in the U200 Lite - sounds like they simply forgot that there’s something like that in the Matter specs.

The Nuki fingerprints are in the “new-matter-lock” as you know.

What’s really missing is a fitting capability. Otherwise I would have submitted a PR to at least raise some awareness. As you can see, it’s just an additional profile and a few lines added, not deleted or modified.

Interestingly, the Nuki Ultra uses a Bluetooth contact sensor that is connected to the lock and the U200 Lite uses an internal gyroscope.

Yes, but that’s using an alternative capability, I was going to check if we could make a request to create one that has those possible Enums. (this can take longer)
But just to be clear, you’re request for now is to “contactSensor” instead, correct?

I’ve only used the contactSensor, because there is no doorState capability yet. And I wouldn’t recommend the contactSensor, because it is a binary state and every other state would be missing.

Implementing the DoorState without a matching enum capability would be a bad idea. You can discuss it internally how such a capability should look like.

So my request is to create a DoorState capability and the implementation of the Matter DoorState feature in the Matter lock driver.

ah ok, I got confused because you mentioned a few times, it was only a few changes and creating a new capability is a complex process (if the request gets approved).
So I just want to make sure that we’re in the same understanding.

1 Like

BTW: thank you so much for responding. It would be awesome if this gets implemented.

1 Like