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