From 9405cda8adc8b170e723c58953c5b6403e6cea57 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 13 May 2026 13:49:08 -0500 Subject: [PATCH 01/19] CHAD-16991: Removed zwave-button apiv6 subdriver --- .../src/apiv6_bugfix/can_handle.lua | 16 ------------- .../zwave-button/src/apiv6_bugfix/init.lua | 24 ------------------- .../zwave-button/src/sub_drivers.lua | 1 - 3 files changed, 41 deletions(-) delete mode 100644 drivers/SmartThings/zwave-button/src/apiv6_bugfix/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua diff --git a/drivers/SmartThings/zwave-button/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-button/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 8a9b8cc6cc..0000000000 --- a/drivers/SmartThings/zwave-button/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle(opts, driver, device, cmd, ...) - local cc = require "st.zwave.CommandClass" - local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION then - return true, require("apiv6_bugfix") - end - return false -end - -return can_handle diff --git a/drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua deleted file mode 100644 index fd309d8e29..0000000000 --- a/drivers/SmartThings/zwave-button/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,24 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), - shared_device_thread_enabled = true, -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-button/src/sub_drivers.lua b/drivers/SmartThings/zwave-button/src/sub_drivers.lua index 57e87ad8ad..64a9813468 100644 --- a/drivers/SmartThings/zwave-button/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-button/src/sub_drivers.lua @@ -4,6 +4,5 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { lazy_load_if_possible("zwave-multi-button"), - lazy_load_if_possible("apiv6_bugfix"), } return sub_drivers From 449e2798d0c03c252d93ce078bde74bd547a5c0d Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 13 May 2026 13:51:17 -0500 Subject: [PATCH 02/19] CHAD-16991: Removed zwave-lock apiv6 subdriver --- .../src/apiv6_bugfix/can_handle.lua | 16 -------------- .../zwave-lock/src/apiv6_bugfix/init.lua | 21 ------------------- .../zwave-lock/src/sub_drivers.lua | 1 - 3 files changed, 38 deletions(-) delete mode 100644 drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 8a9b8cc6cc..0000000000 --- a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle(opts, driver, device, cmd, ...) - local cc = require "st.zwave.CommandClass" - local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION then - return true, require("apiv6_bugfix") - end - return false -end - -return can_handle diff --git a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua deleted file mode 100644 index 94dc5975ab..0000000000 --- a/drivers/SmartThings/zwave-lock/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,21 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua index 46700ce154..627fe99514 100644 --- a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua @@ -7,6 +7,5 @@ local sub_drivers = { lazy_load_if_possible("schlage-lock"), lazy_load_if_possible("samsung-lock"), lazy_load_if_possible("keywe-lock"), - lazy_load_if_possible("apiv6_bugfix"), } return sub_drivers From a80ca3d3e5a5520ece73a3e4fdc83cad523d9f1b Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 13 May 2026 13:51:17 -0500 Subject: [PATCH 03/19] CHAD-16991: Removed zwave-sensor apiv6 subdriver --- .../src/apiv6_bugfix/can_handle.lua | 35 ------------------- .../zwave-sensor/src/apiv6_bugfix/init.lua | 22 ------------ .../zwave-sensor/src/sub_drivers.lua | 1 - 3 files changed, 58 deletions(-) delete mode 100644 drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua diff --git a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 4913e9a25e..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,35 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - --- doing refresh would cause incorrect state for device, see comments in wakeup-no-poll -local NORTEK_FP = {mfr = 0x014F, prod = 0x2001, model = 0x0102} -- NorTek open/close sensor -local POPP_THERMOSTAT_FP = {mfr = 0x0002, prod = 0x0115, model = 0xA010} --Popp thermostat -local AEOTEC_MULTISENSOR_6_FP = {mfr = 0x0086, model = 0x0064} --Aeotec multisensor 6 -local AEOTEC_MULTISENSOR_7_FP = {mfr = 0x0371, model = 0x0018} --Aeotec multisensor 7 -local ENERWAVE_MOTION_FP = {mfr = 0x011A} --Enerwave motion sensor -local HOMESEER_MULTI_SENSOR_FP = {mfr = 0x001E, prod = 0x0002, model = 0x0001} -- Homeseer multi sensor HSM100 -local SENSATIVE_STRIP_FP = {mfr = 0x019A, model = 0x000A} -local FPS = {NORTEK_FP, POPP_THERMOSTAT_FP, - AEOTEC_MULTISENSOR_6_FP, AEOTEC_MULTISENSOR_7_FP, - ENERWAVE_MOTION_FP, HOMESEER_MULTI_SENSOR_FP, SENSATIVE_STRIP_FP} - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION then - - for _, fp in ipairs(FPS) do - if device:id_match(fp.mfr, fp.prod, fp.model) then return false end - end - local subdriver = require("apiv6_bugfix") - return true, subdriver - else - return false - end -end - -return can_handle diff --git a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua deleted file mode 100644 index 38b8a6612f..0000000000 --- a/drivers/SmartThings/zwave-sensor/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,22 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), - shared_device_thread_enabled = true, -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua b/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua index 9504479304..9500731597 100644 --- a/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-sensor/src/sub_drivers.lua @@ -22,5 +22,4 @@ return { lazy_load_if_possible("timed-tamper-clear"), lazy_load_if_possible("wakeup-no-poll"), lazy_load_if_possible("firmware-version"), - lazy_load_if_possible("apiv6_bugfix"), } From d84fe48b69d9c219688d9995ad00ac764337df10 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 13 May 2026 13:51:17 -0500 Subject: [PATCH 04/19] CHAD-16991: Removed zwave-siren apiv6 subdriver --- .../src/apiv6_bugfix/can_handle.lua | 17 -------------- .../zwave-siren/src/apiv6_bugfix/init.lua | 23 ------------------- .../zwave-siren/src/sub_drivers.lua | 1 - 3 files changed, 41 deletions(-) delete mode 100644 drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua diff --git a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 3f4b44c1e0..0000000000 --- a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,17 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle(opts, driver, device, cmd, ...) - local cc = require "st.zwave.CommandClass" - local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION - then - return true, require("apiv6_bugfix") - end - return false -end - -return can_handle diff --git a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua deleted file mode 100644 index 2e7e3ca3b8..0000000000 --- a/drivers/SmartThings/zwave-siren/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,23 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-siren/src/sub_drivers.lua b/drivers/SmartThings/zwave-siren/src/sub_drivers.lua index 12ce423ba5..52d5035a16 100644 --- a/drivers/SmartThings/zwave-siren/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-siren/src/sub_drivers.lua @@ -14,6 +14,5 @@ local sub_drivers = { lazy_load_if_possible("zipato-siren"), lazy_load_if_possible("utilitech-siren"), lazy_load_if_possible("fortrezz"), - lazy_load_if_possible("apiv6_bugfix"), } return sub_drivers From 6480128257c6bfa894a0e40d64d2d3ab9e3c87c2 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 13 May 2026 13:51:17 -0500 Subject: [PATCH 05/19] CHAD-16991: Removed zwave-smoke-alarm apiv6 subdriver --- .../src/apiv6_bugfix/can_handle.lua | 16 -------------- .../src/apiv6_bugfix/init.lua | 21 ------------------- .../zwave-smoke-alarm/src/sub_drivers.lua | 1 - 3 files changed, 38 deletions(-) delete mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 8a9b8cc6cc..0000000000 --- a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,16 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle(opts, driver, device, cmd, ...) - local cc = require "st.zwave.CommandClass" - local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - local version = require "version" - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION then - return true, require("apiv6_bugfix") - end - return false -end - -return can_handle diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua deleted file mode 100644 index 94dc5975ab..0000000000 --- a/drivers/SmartThings/zwave-smoke-alarm/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,21 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua b/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua index f0a2d96412..39901aaf21 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/sub_drivers.lua @@ -6,6 +6,5 @@ local sub_drivers = { lazy_load_if_possible("zwave-smoke-co-alarm-v1"), lazy_load_if_possible("zwave-smoke-co-alarm-v2"), lazy_load_if_possible("fibaro-smoke-sensor"), - lazy_load_if_possible("apiv6_bugfix"), } return sub_drivers From 24d35057d28ee217edcd102946e05aa1e30d3173 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 13 May 2026 13:51:17 -0500 Subject: [PATCH 06/19] CHAD-16991: Removed zwave-thermostat apiv6 subdriver --- .../src/apiv6_bugfix/can_handle.lua | 25 ------------------- .../src/apiv6_bugfix/fingerprints.lua | 9 ------- .../src/apiv6_bugfix/init.lua | 22 ---------------- .../zwave-thermostat/src/sub_drivers.lua | 1 - 4 files changed, 57 deletions(-) delete mode 100644 drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua delete mode 100644 drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua delete mode 100644 drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua deleted file mode 100644 index 079eeee5d3..0000000000 --- a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/can_handle.lua +++ /dev/null @@ -1,25 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle(opts, driver, device, cmd, ...) - local version = require "version" - local cc = require "st.zwave.CommandClass" - local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - local DANFOSS_LC13_THERMOSTAT_FPS = require "apiv6_bugfix.fingerprints" - - if version.api == 6 and - cmd.cmd_class == cc.WAKE_UP and - cmd.cmd_id == WakeUp.NOTIFICATION and not - (device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[1].manufacturerId, - DANFOSS_LC13_THERMOSTAT_FPS[1].productType, - DANFOSS_LC13_THERMOSTAT_FPS[1].productId) or - device:id_match(DANFOSS_LC13_THERMOSTAT_FPS[2].manufacturerId, - DANFOSS_LC13_THERMOSTAT_FPS[2].productType, - DANFOSS_LC13_THERMOSTAT_FPS[2].productId)) then - return true, require "apiv6_bugfix" - else - return false - end -end - -return can_handle diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua deleted file mode 100644 index e87a5990e2..0000000000 --- a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/fingerprints.lua +++ /dev/null @@ -1,9 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local DANFOSS_LC13_THERMOSTAT_FPS = { - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0003 }, -- Danfoss LC13 Thermostat - { manufacturerId = 0x0002, productType = 0x0005, productId = 0x0004 } -- Danfoss LC13 Thermostat -} - -return DANFOSS_LC13_THERMOSTAT_FPS diff --git a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua b/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua deleted file mode 100644 index 52c419a590..0000000000 --- a/drivers/SmartThings/zwave-thermostat/src/apiv6_bugfix/init.lua +++ /dev/null @@ -1,22 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local cc = require "st.zwave.CommandClass" -local WakeUp = (require "st.zwave.CommandClass.WakeUp")({ version = 1 }) - - -local function wakeup_notification(driver, device, cmd) - device:refresh() -end - -local apiv6_bugfix = { - zwave_handlers = { - [cc.WAKE_UP] = { - [WakeUp.NOTIFICATION] = wakeup_notification - } - }, - NAME = "apiv6_bugfix", - can_handle = require("apiv6_bugfix.can_handle"), -} - -return apiv6_bugfix diff --git a/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua index dc30091b79..38b6d5de87 100644 --- a/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-thermostat/src/sub_drivers.lua @@ -10,6 +10,5 @@ local sub_drivers = { lazy_load_if_possible("stelpro-ki-thermostat"), lazy_load_if_possible("qubino-flush-thermostat"), lazy_load_if_possible("thermostat-heating-battery"), - lazy_load_if_possible("apiv6_bugfix"), } return sub_drivers From 758245bf208ac02d89fedac08faaf558352e65a0 Mon Sep 17 00:00:00 2001 From: noidlet Date: Mon, 18 May 2026 10:46:11 -0400 Subject: [PATCH 07/19] Fix duplicate profile check to handle deleted files The check_duplicates.py script was failing when a PR deleted a profile file. The script would try to open the deleted file for comparison, causing a FileNotFoundError. This fix adds an existence check before attempting to open the file, skipping deleted files with an informative message. --- .github/scripts/check_duplicates.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/scripts/check_duplicates.py b/.github/scripts/check_duplicates.py index b4bf01649d..ae0a26f687 100644 --- a/.github/scripts/check_duplicates.py +++ b/.github/scripts/check_duplicates.py @@ -112,9 +112,15 @@ def compare_components(prof1, prof2): print('\nNEW PROFILE:\n%s is a profile! Comparing to other profiles...' % file) os.chdir(file_directory) - for current_profile in os.listdir("./"): - new_profile = file_basename + new_profile = file_basename + + # Skip deleted files + if not os.path.exists(new_profile): + print("Skipping %s - file was deleted" % new_profile) + os.chdir(cwd) + continue + for current_profile in os.listdir("./"): # compare to YAML files that are not the same file # Compare only .yml files and only files that have not already been found to be a duplicate if current_profile != new_profile and Path(current_profile).suffix == ".yml" and (current_profile, new_profile) not in duplicate_pairs: From 732cc20f54b1c5067828d818b94432ea1f3f8b2c Mon Sep 17 00:00:00 2001 From: noidlet Date: Mon, 18 May 2026 11:15:52 -0400 Subject: [PATCH 08/19] Emit warning when profile files are deleted in PR --- .github/scripts/check_duplicates.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/scripts/check_duplicates.py b/.github/scripts/check_duplicates.py index ae0a26f687..65e2ae2ea6 100644 --- a/.github/scripts/check_duplicates.py +++ b/.github/scripts/check_duplicates.py @@ -5,6 +5,7 @@ cwd = os.getcwd() duplicate_pairs = [] +deleted_profiles = [] def compare_component_capabilities_unordered(comp1, comp2): for cap1 in comp1["capabilities"]: @@ -114,9 +115,10 @@ def compare_components(prof1, prof2): os.chdir(file_directory) new_profile = file_basename - # Skip deleted files + # Skip deleted files and track them for warning if not os.path.exists(new_profile): print("Skipping %s - file was deleted" % new_profile) + deleted_profiles.append(file) os.chdir(cwd) continue @@ -151,7 +153,12 @@ def compare_components(prof1, prof2): for duplicate in duplicate_pairs: f.write("%s == %s\n" % (duplicate[0], duplicate [1])) else: - f.write("Duplicate profile check: Passed - no duplicate profiles detected.") + f.write("Duplicate profile check: Passed - no duplicate profiles detected.\n") + + if deleted_profiles: + f.write("\n:warning: **Deleted profile files detected:**\n") + for deleted in deleted_profiles: + f.write("- `%s`\n" % deleted) with open("profile-comment-body.md", "r") as f: print("\n" + f.read()) From 1607bb32b46f86077fd8d2300cbe7d50b687410b Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 15 May 2026 12:25:13 -0500 Subject: [PATCH 09/19] Add AGENTS.md and a set of skills to help agents in this repository These are based on what I have setup for myself locally, but are not exactly the same, so are somewhat untested. Some of the skills and potentially even the AGENTS.md file may be too large for very small models, but I think this will really help anyone using tools like opencode, claude code, codex, etc when working with agents in this repo. --- .agents/skills/dev-workflow/SKILL.md | 264 +++++++++++++ .agents/skills/linting-and-style/SKILL.md | 96 +++++ .agents/skills/testing-edge-drivers/SKILL.md | 349 +++++++++++++++++ .../understanding-lua-libraries/SKILL.md | 295 ++++++++++++++ .../skills/understanding-profiles/SKILL.md | 369 ++++++++++++++++++ .github/copilot-instructions.md | 52 +++ AGENTS.md | 130 ++++++ 7 files changed, 1555 insertions(+) create mode 100644 .agents/skills/dev-workflow/SKILL.md create mode 100644 .agents/skills/linting-and-style/SKILL.md create mode 100644 .agents/skills/testing-edge-drivers/SKILL.md create mode 100644 .agents/skills/understanding-lua-libraries/SKILL.md create mode 100644 .agents/skills/understanding-profiles/SKILL.md create mode 100644 .github/copilot-instructions.md create mode 100644 AGENTS.md diff --git a/.agents/skills/dev-workflow/SKILL.md b/.agents/skills/dev-workflow/SKILL.md new file mode 100644 index 0000000000..23f4ce17df --- /dev/null +++ b/.agents/skills/dev-workflow/SKILL.md @@ -0,0 +1,264 @@ +--- +name: dev-workflow +description: Setting up the development environment, deploying Edge Drivers to hubs, and sharing drivers with other users via channels and invites +--- + +# SmartThings Edge Driver Development Workflow + +This skill covers environment setup, driver deployment to hubs, and sharing +drivers with other users through channels and invite links. + +--- + +## Environment Setup + +### 1. Install Lua 5.3 + +Edge Drivers are Lua-based. Install the Lua 5.3 runtime for local development +and linting: + +```bash +# Ubuntu / Debian +sudo apt install lua5.3 + +# macOS +brew install lua@5.3 + +# Windows +# Download the Lua 5.3 binary from https://luabinaries.sourceforge.net/download.html +# Or install via scoop: +scoop install lua +# Or via chocolatey: +choco install lua53 +``` + +### 2. lua_libs Directory + +The `lua_libs/` directory contains the SmartThings Lua libraries that are +available on the hub at runtime. These correspond to the assets attached to the +latest release on GitHub: + + + +Download the lua_libs archive from the release assets and +extract it into the repository root if it is missing or needs updating. + +### 3. Configure LUA_PATH + +Set `LUA_PATH` so that `require` resolves both your driver modules and the +SmartThings library modules in `lua_libs/`: + +```bash +export LUA_PATH="./?.lua;./?/init.lua;$(pwd)/lua_libs/?.lua;$(pwd)/lua_libs/?/init.lua;;" +``` + +Run it from the repository root so `$(pwd)` resolves correctly. + + +### 4. Install the SmartThings CLI + +The CLI is required for packaging, deploying, and managing drivers and +channels on the platform. + +```bash +# Via npm (requires Node.js >= 24.8.0) +npm install -g @smartthings/cli + +# macOS via Homebrew +brew install smartthingscommunity/smartthings/smartthings + +# Linux / Windows +# Download the binary or installer from: +# https://github.com/SmartThingsCommunity/smartthings-cli/releases +``` + +Verify the installation: + +```bash +smartthings --version +``` + +The CLI uses browser-based OAuth login by default. Run `smartthings devices` to trigger +the login flow. + +### 5. Python Requirements (Testing) + +Some test and tooling scripts require Python dependencies: + +```bash +pip install -r tools/requirements.txt +``` + +### 6. Install Luacheck (Linting) + +Luacheck provides static analysis for Lua source files. It requires LuaRocks +(the Lua package manager). + +**Install LuaRocks first:** + +```bash +# Ubuntu / Debian +sudo apt install luarocks + +# macOS +brew install luarocks + +# Windows +# Download the installer from https://luarocks.org/releases/ +# Or via chocolatey: +choco install luarocks +``` + +**Then install Luacheck:** + +```bash +# Via LuaRocks (all platforms) +luarocks install luacheck + +# macOS alternative (installs both luarocks and luacheck) +brew install luacheck +``` + +Run it against a driver directory: + +```bash +luacheck --config .github/workflows/.luacheckrc drivers/SmartThings/zigbee-switch/ +``` + +--- + +## Deploying Drivers + +### Overview + +Deploying a driver to a physical hub requires three things: + +1. A **channel** you own. +2. The hub **enrolled** in that channel. +3. The driver **packaged and uploaded** through the CLI. + +### Step 1: Create a Channel + +```bash +smartthings edge:channels:create +``` + +You will be prompted for a name and description. Note the returned channel ID. + +### Step 2: Enroll Your Hub + +```bash +smartthings edge:channels:enroll +``` + +Select the channel when prompted, or pass `--channel `. + +Find your hub ID with: + +```bash +smartthings devices --type HUB +``` + +### Step 3: Package and Install the Driver + +The `edge:drivers:package` command can build, upload, assign to a channel, and +install in one step: + +```bash +smartthings edge:drivers:package \ + --hub= \ + --channel= +``` + +For example: + +```bash +smartthings edge:drivers:package drivers/SmartThings/zwave-switch \ + --hub=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee \ + --channel=11111111-2222-3333-4444-555555555555 +``` + +### Other Useful Deployment Commands + +```bash +# List drivers installed on a hub +smartthings edge:drivers:installed --hub= + +# Stream logs from a driver on the hub +smartthings edge:drivers:logcat --hub= + +# Uninstall a driver from a hub +smartthings edge:drivers:uninstall --hub= + +# Remove unused drivers from a hub +smartthings edge:drivers:prune --hub= + +# Switch a device to a different driver +smartthings edge:drivers:switch +``` + +--- + +## Sharing Drivers + +### Creating an Invite Link + +Invite links let other users install your driver from your channel without +giving them ownership of the driver or channel. + +```bash +smartthings edge:channels:invites:create +``` + +You will be prompted to select a channel and a driver. The command returns an +invite URL of the form: + +``` +https://bestow-regional.api.smartthings.com/invite/ +``` + +Share this URL with users. They open it in a browser or the SmartThings mobile +app to accept the invitation. + +### Enrollment Flow for Recipients + +1. The recipient opens the invite link. +2. They log in to their Samsung / SmartThings account. +3. They select a hub to enroll in the channel. +4. The driver can be selected to install to that hub. + +### Managing Invites + +```bash +# List existing invites +smartthings edge:channels:invites + +# Delete an invite +smartthings edge:channels:invites:delete +``` + +### Managing Channel Assignments + +```bash +# Assign a specific driver version to a channel +smartthings edge:channels:assign + +# List drivers assigned to a channel +smartthings edge:channels:drivers + +# Remove a driver from a channel +smartthings edge:channels:unassign +``` + +--- + +## Quick Reference + +| Task | Command | +|------|---------| +| Create channel | `smartthings edge:channels:create` | +| Enroll hub | `smartthings edge:channels:enroll ` | +| Package & deploy | `smartthings edge:drivers:package --hub= --channel=` | +| Stream logs | `smartthings edge:drivers:logcat --hub=` | +| Create invite | `smartthings edge:channels:invites:create` | +| List installed drivers | `smartthings edge:drivers:installed --hub=` | diff --git a/.agents/skills/linting-and-style/SKILL.md b/.agents/skills/linting-and-style/SKILL.md new file mode 100644 index 0000000000..83578854ce --- /dev/null +++ b/.agents/skills/linting-and-style/SKILL.md @@ -0,0 +1,96 @@ +--- +name: linting-and-style +description: Running luacheck for Lua linting and following code style conventions in Edge Driver development +--- + +# Linting and Code Style for Edge Drivers + +## Running Luacheck + +```bash +luacheck --config .github/workflows/.luacheckrc +``` + +### Examples + +```bash +# Lint a specific driver +luacheck --config .github/workflows/.luacheckrc drivers/SmartThings/zigbee-switch/ + +# Lint a single file +luacheck --config .github/workflows/.luacheckrc drivers/SmartThings/zigbee-switch/src/init.lua + +# Lint the entire repo +luacheck --config .github/workflows/.luacheckrc . +``` + +Luacheck runs automatically in CI on pull requests that modify files under `drivers/` (see `.github/workflows/luacheck.yml`). + +## Code Style Conventions + +These conventions are observed across the Edge Driver codebase: + +### General + +- **Indentation**: 2 spaces, no tabs +- **Strings**: Use double quotes `"string"` for module requires and general strings +- **Local variables**: Always use `local` for variables and functions at module scope +- **Line length**: No enforced limit, but most code stays under 120 characters + +### Naming + +- **Variables and functions**: `snake_case` (e.g., `local mock_device`, `local function test_init()`) +- **Constants**: `UPPER_SNAKE_CASE` for true constants (e.g., `SENSOR_BINARY`) +- **Modules**: Return a table at the end of the file (`return module_name`) + +### Requires and Imports + +```lua +-- Standard library requires first +local capabilities = require "st.capabilities" +local zw = require "st.zwave" + +-- Then test/integration requires +local test = require "integration_test" +local t_utils = require "integration_test.utils" + +-- Then protocol-specific requires +local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) +``` + +### Function Style + +- Prefer `local function name()` over `local name = function()` +- Handler functions typically receive `(driver, device, ...)` arguments +- Use early returns for guard clauses + +### Tables + +- Trailing commas are common and acceptable in multi-line tables +- Align table entries for readability in test manifests + +### Comments + +- Use `--` for single-line comments +- Minimal inline comments; code should be self-documenting + +Copyright header at the top of every file: +```lua +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +``` + +### File Organization for Drivers + +``` +driver-name/ + src/ + init.lua -- Main driver entry point + .lua -- Additional driver modules + test/ + test_*.lua -- Test files (must start with test_) + profiles/ + *.yml -- Device profiles + fingerprints.yml -- Device fingerprints + config.yml -- Driver configuration +``` diff --git a/.agents/skills/testing-edge-drivers/SKILL.md b/.agents/skills/testing-edge-drivers/SKILL.md new file mode 100644 index 0000000000..a34a22c901 --- /dev/null +++ b/.agents/skills/testing-edge-drivers/SKILL.md @@ -0,0 +1,349 @@ +--- +name: testing-edge-drivers +description: Running and writing integration tests for SmartThings Edge Drivers using the Python test harness and Lua integration test framework +--- + +# Testing SmartThings Edge Drivers + +## Running Tests + +Tests are run via the Python test harness: + +```bash +python3 tools/run_driver_tests.py [options] +``` + +### Options + +| Flag | Description | +|------|-------------| +| `-v` | Print individual test names and pass/fail status | +| `-vv` | Print test names, status, and full logs on failures (recommended) | +| `-vvv` | Print all logs from all tests | +| `-f ` | Only run tests whose file path matches the regex filter | +| `-j ` | Output JUnit XML results to the specified file | +| `-c [files]` | Run with luacov code coverage | +| `--html` | Generate HTML coverage reports (use with `-c`) | + +### Filter Examples + +```bash +# Run all tests for a specific driver +python3 tools/run_driver_tests.py -vv -f "zwave-smoke-alarm" + +# Run a specific test file +python3 tools/run_driver_tests.py -vv -f "test_zwave_smoke_detector" + +# Run all zigbee switch tests +python3 tools/run_driver_tests.py -vv -f "zigbee-switch" + +# Run all virtual device tests +python3 tools/run_driver_tests.py -vv -f "virtual" +``` + +The filter is a regex applied to the full file path. The harness searches for files matching `drivers/*/*/src/test/test_*.lua`. + +### Python Requirements + +Install dependencies before running tests: + +```bash +pip install -r tools/requirements.txt +``` + +Required packages: `junit_xml`, `requests`, `PyYAML`, `regex`. + +### How Tests Execute + +The Python harness (`tools/run_driver_tests.py`): +1. Globs for all `test_*.lua` files under `drivers/*/src/test/` +2. Filters by the `-f` regex if provided +3. Changes directory to the driver's `src/` directory (two levels up from the test file) +4. Runs each test file with `lua ` +5. Parses stdout for `Running test`, `PASSED`, `FAILED`, and summary lines +6. Reports totals and exits with code 1 if any tests failed + +## Integration Test Framework + +The framework lives in `lua_libs/integration_test/` and is required as `integration_test` in test files. It provides: + +### Core Modules + +| Module | Purpose | +|--------|---------| +| `integration_test` (init.lua) | Main test runner, registration, mock device builder | +| `integration_test.utils` | Utility functions like `get_profile_definition()` | +| `integration_test.mock_device` | Build mock Zigbee, Z-Wave, Matter, or generic devices | +| `integration_test.zwave_test_utils` | Z-Wave specific helpers (e.g., `zwave_test_build_receive_command`) | +| `integration_test.zigbee_test_utils` | Zigbee specific helpers | +| `integration_test.mock_socket` | Mock socket layer with channel-based message routing | + +### Channels + +The test framework uses channels to simulate communication between the driver and the platform: + +- `zwave` - Z-Wave protocol messages +- `zigbee` - Zigbee protocol messages +- `matter` - Matter protocol messages +- `capability` - SmartThings capability events (commands from cloud, events to cloud) +- `device_lifecycle` - Device lifecycle events (init, added, removed, etc.) +- `driver_lifecycle` - Driver lifecycle events +- `timer` - Timer-related events + +Each channel supports two directions: +- `receive` - Messages sent TO the driver (incoming commands, device reports) +- `send` - Messages sent FROM the driver (capability events, protocol commands) + +## Writing Tests + +### Test File Structure + +Every test file follows this pattern: + +```lua +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +-- 1. Build mock device(s) +local mock_device = test.mock_device.build_test_generic_device({ + profile = t_utils.get_profile_definition("my-profile.yml"), +}) + +-- 2. Define test init function (runs before each test) +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +-- 3. Register tests (message tests or coroutine tests) + +-- 4. Run all registered tests +test.run_registered_tests() +``` + +### Building Mock Devices + +```lua +-- Generic device (no protocol) +local mock = test.mock_device.build_test_generic_device({ + profile = t_utils.get_profile_definition("profile-name.yml"), + preferences = { ["certifiedpreferences.somePref"] = true }, +}) + +-- Z-Wave device +local zw = require "st.zwave" +local mock = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("profile-name.yml"), + zwave_endpoints = { + { + command_classes = { + { value = zw.SENSOR_BINARY }, + { value = zw.NOTIFICATION }, + } + } + } +}) + +-- Zigbee device +local mock = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("profile-name.yml"), + zigbee_endpoints = { ... } +}) +``` + +### Message Tests (`register_message_test`) + +Message tests define an ordered sequence of receive/send message pairs. Each receive triggers the driver handler, and the subsequent sends are the expected outputs. + +```lua +test.register_message_test( + "Test description", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "switch", component = "main", command = "on", args = {} } + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + } + }, + { + min_api_version = 17 -- optional version constraint + } +) +``` + +The manifest is an array of message entries. The framework groups them into blocks: each block starts with a `receive` followed by zero or more `send` entries. The receives are queued on the mock channel; the sends are set as expectations. The driver processes the receive and the framework asserts the expected sends occurred. + +### Coroutine Tests (`register_coroutine_test`) + +For more complex test logic (multiple interactions, state changes, conditional assertions, timer manipulation): + +```lua +test.register_coroutine_test( + "Test with complex logic", + function() + -- Queue a lifecycle event + test.socket.device_lifecycle():__queue_receive({ mock_device.id, "init" }) + test.socket.device_lifecycle():__queue_receive( + mock_device:generate_info_changed({ + preferences = { ["certifiedpreferences.somePref"] = false } + }) + ) + test.wait_for_events() + + -- Now send a capability command and expect a response + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "switch", component = "main", command = "on", args = {} } + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.switch.switch.on()) + ) + end, + { + min_api_version = 17 + } +) +``` + +Key coroutine test APIs: +- `test.socket.:__queue_receive(msg)` - Queue a message for the driver to receive +- `test.socket.:__expect_send(msg)` - Set an expectation for a message the driver should send +- `test.wait_for_events()` - Yield to let the driver process queued messages and check expectations +- `test.mock_time.advance_time(seconds)` - Advance the mock clock + +### Real Example: Z-Wave Smoke Detector Test + +From `drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua`: + +```lua +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" + +local SensorBinary = (require "st.zwave.CommandClass.SensorBinary")({ version = 2 }) + +local sensor_endpoints = { + { + command_classes = { + { value = zw.SENSOR_BINARY }, + { value = zw.SENSOR_ALARM }, + { value = zw.NOTIFICATION }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("smoke-battery-temperature-tamperalert-temperaturealarm.yml"), + zwave_endpoints = sensor_endpoints +}) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_message_test( + "Sensor Binary report (smoke) should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + zw_test_utils.zwave_test_build_receive_command( + SensorBinary:Report({ + sensor_type = SensorBinary.sensor_type.SMOKE, + sensor_value = SensorBinary.sensor_value.DETECTED_AN_EVENT + }) + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", + capabilities.smokeDetector.smoke.detected()) + } + }, + { min_api_version = 17 } +) + +test.run_registered_tests() +``` + +## Common Test Patterns + +### Testing Capability Commands (cloud -> device) + +Receive on `capability` channel, expect protocol message on `zwave`/`zigbee`/`matter`: + +```lua +{ + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = {} } } +}, +{ + channel = "zwave", + direction = "send", + message = ... -- expected Z-Wave command +} +``` + +### Testing Device Reports (device -> cloud) + +Receive on protocol channel, expect capability event on `capability`: + +```lua +{ + channel = "zwave", + direction = "receive", + message = { mock_device.id, zw_test_utils.zwave_test_build_receive_command(...) } +}, +{ + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) +} +``` + +### Testing Lifecycle Events + +```lua +test.socket.device_lifecycle():__queue_receive({ mock_device.id, "added" }) +test.socket.device_lifecycle():__queue_receive({ mock_device.id, "init" }) +test.socket.device_lifecycle():__queue_receive({ mock_device.id, "doConfigure" }) +``` + +### Testing Preference Changes + +```lua +test.socket.device_lifecycle():__queue_receive( + mock_device:generate_info_changed({ + preferences = { ["certifiedpreferences.myPref"] = new_value } + }) +) +``` + +### Optional Test Parameters + +The `opts` table passed to `register_message_test` or `register_coroutine_test` supports: + +| Field | Description | +|-------|-------------| +| `min_api_version` | Skip test if API version is below this (commonly set to 17) | +| `max_api_version` | Skip test if API version is above this | +| `test_init` | Per-test init function (overrides the global `set_test_init_function`) | +| `expected_error` | String or array of Lua patterns for expected errors | +| `inner_block_ordering` | Set to `"relaxed"` to allow sends in any order within a block | diff --git a/.agents/skills/understanding-lua-libraries/SKILL.md b/.agents/skills/understanding-lua-libraries/SKILL.md new file mode 100644 index 0000000000..235f637b69 --- /dev/null +++ b/.agents/skills/understanding-lua-libraries/SKILL.md @@ -0,0 +1,295 @@ +--- +name: understanding-lua-libraries +description: Understanding the SmartThings Edge Driver Lua libraries - driver lifecycle, message dispatchers, default handlers, and protocol message objects +--- + +# SmartThings Edge Driver Lua Library Architecture + +## 1. Driver Initialization and Run Loop + +A driver is created by calling `Driver("name", template)` (or a protocol-specific variant like `ZigbeeDriver("name", template)`). The template is a Lua table containing handler tables and configuration. + +The base `Driver.init` (in `lua_libs/st/driver.lua`) does the following: +- Sets `out_driver.NAME` from the name argument +- Initializes handler tables: `capability_handlers`, `lifecycle_handlers`, `message_handlers` +- Opens communication channels via cosock sockets: `capability_channel`, `environment_channel`, `lifecycle_channel`, `driver_lifecycle_channel`, and optionally `discovery_channel` +- Initializes a datastore and device cache tables +- Calls `Driver.standardize_sub_drivers()` to normalize the `sub_drivers` list +- Builds the `lifecycle_dispatcher` and `capability_dispatcher` from handlers + sub_drivers +- Registers channel handlers so inbound messages get routed to the correct handler function + +The `driver:run()` call starts the cosock event loop, which runs forever processing messages from all registered channels. + +## 2. Message Dispatchers + +The dispatcher system (`lua_libs/st/dispatcher.lua`) is a hierarchical message routing tree. The base class `MessageDispatcher` provides: + +- **`default_handlers`** - handlers at this level of the hierarchy. +- **`child_dispatchers`** - sub-dispatchers (from sub_drivers) that may override defaults +- **`can_handle(driver, device, ...)`** - returns true if this dispatcher or a child can handle the message +- **`dispatch(driver, device, ...)`** - finds and executes the matching handler + +### Dispatch logic + +1. The dispatcher calls `can_handle` on each child dispatcher +2. If any children can handle: **only the children handle it** (parent defaults are NOT called) +3. If multiple children match: ALL matching children receive the message +4. If NO children match: parent defaults are used +5. This is recursive -- sub-drivers can have sub-drivers + +### Dispatcher types + +| Dispatcher | Class | Handles | +|------------|-------|---------| +| `capability_dispatcher` | `CapabilityCommandDispatcher` | Capability commands from the platform (on, off, setLevel, etc.) | +| `lifecycle_dispatcher` | `DeviceLifecycleDispatcher` | Device lifecycle events (added, init, removed, etc.) | +| `zigbee_message_dispatcher` | `ZigbeeMessageDispatcher` | Incoming Zigbee messages (attribute reports, cluster commands, ZDO) | +| `zwave_dispatcher` | `ZwaveDispatcher` | Incoming Z-Wave commands | +| `matter_dispatcher` | `MatterMessageDispatcher` | Incoming Matter interaction responses | +| `secret_data_dispatcher` | `SecretDataDispatcher` | Security/secret data events | + +Each protocol-specific driver (ZigbeeDriver, ZwaveDriver, MatterDriver) adds its own dispatcher on top of the base Driver's capability and lifecycle dispatchers. + +**Zigbee handler structure:** +```lua +zigbee_handlers = { + attr = { -- attribute reports / read responses + [ClusterID] = { + [AttributeID] = handler_function, + } + }, + global = { -- global ZCL commands + [ClusterID] = { + [CommandID] = handler_function, + } + }, + cluster = { -- cluster-specific commands + [ClusterID] = { + [CommandID] = handler_function, + } + }, + zdo = { -- ZDO commands + [ClusterID] = handler_function, + } +} +``` + +**Z-Wave handler structure:** +```lua +zwave_handlers = { + [cc.SWITCH_BINARY] = { -- command class + [SwitchBinary.REPORT] = handler_function, -- command ID + }, +} +``` + +**Matter handler structure:** +```lua +matter_handlers = { + attr = { + [ClusterID] = { + [AttributeID] = handler_function, + } + }, + cmd_response = { ... }, + event = { ... }, + fallback = handler_function, +} +``` + +**Capability handler structure:** +```lua +capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = handle_on, + [capabilities.switch.commands.off.NAME] = handle_off, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = handle_set_level, + }, +} +``` + +## 3. Sub-Drivers Pattern + +Sub-drivers allow device-specific behavior overrides gated by a `can_handle` function. A sub-driver is a table with: +- `NAME` (string) +- `can_handle(opts, driver, device, ...) -> boolean` +- Protocol handlers (zigbee_handlers, zwave_handlers, matter_handlers) +- `capability_handlers`, `lifecycle_handlers` +- Optional nested `sub_drivers` + +In practice, sub-drivers are often organized as separate files under `src/sub_drivers/` for clarity, and required in the main driver template. + + +### Dispatch Logic + +1. The dispatcher calls `can_handle` on each child dispatcher +2. If any children can handle: **only the children handle it** (parent defaults are NOT called) +3. If multiple children match: ALL matching children receive the message +4. If NO children match: parent defaults are used +5. This is recursive -- sub-drivers can have sub-drivers + +### Lazy Loading + +Sub-drivers support lazy loading for memory optimization: +- `Driver.lazy_load_sub_driver(sub_driver)`: Strips handlers, keeps only `can_handle` and `NAME` +- `Driver.lazy_load_sub_driver_v2(require_path)`: Even more efficient; only requires `can_handle` and `sub_drivers` modules separately +- A sub-driver with no handlers defined is automatically treated as lazy-loadable + +New sub-drivers must be: +1. Listed in the parent's `sub_drivers.lua` (or the equivalent sub_drivers table) +2. Have a `can_handle.lua` that correctly identifies the target devices +3. Have an `init.lua` that returns the sub-driver table + +If any of these are missing, the sub-driver will not be loaded. + + +## 4. Lifecycle Events + +Device lifecycle events are dispatched through the `DeviceLifecycleDispatcher`. The key events: + +1. **`init`** -- Called for every device on driver startup (existing devices) and after `added` for new devices. Used for setting up component/endpoint mappings and device fields. +2. **`added`** -- Called only when a device is first paired. NOT called for existing devices when a driver is updated. After `added`, a synthetic `init` is automatically dispatched. +3. **`doConfigure`** -- Called when the device needs configuration (typically after pairing). +4. **`infoChanged`** -- Called when device metadata changes (e.g., preferences updated). Receives `args.old_st_store` for comparison. +5. **`removed`** -- Called when device is removed. +6. **`driverSwitched`** -- Called when device switches to this driver. + +Register lifecycle handlers in the template: +```lua +lifecycle_handlers = { + init = device_init, + added = device_added, + removed = device_removed, + doConfigure = device_do_configure, + infoChanged = info_changed_handler, +} +``` + +Handler signature: `function(driver, device, event, args)` + +**Default behaviors provided by the framework:** +- `driverSwitched`: Base Driver marks device as `NONFUNCTIONAL`. ZigbeeDriver overrides this to check capability matching and marks as `PROVISIONED` if all capabilities match. +- `doConfigure`: ZigbeeDriver defaults to `device_management.configure` which sends attribute reporting configuration. +- `added`: After a successful `added` callback, the framework automatically queues a synthetic `init` event. +- `doConfigure`: After success, the framework transitions the device to `PROVISIONED` state. +- Unhandled lifecycle events log a trace message and are otherwise ignored (fallback handler). + +**Critical timing knowledge for lifecycle events** + +1. **`init` on driver startup**. After hub restart the radio may not be ready and sending Zigbee/Z-Wave commands in `init` can fail. +2. **`added` is NOT called for existing devices** on driver update. Only called on first pair. Code that must run for existing devices should go in `init` (for non-radio operations) or use `driverSwitched`. +3. **`doConfigure` is called any time a device is added with the TYPED provisioning state** and is the right place for device-specific configuration commands. +4. **`infoChanged` receives `args.old_st_store`** for comparing old vs new preferences. Drivers should check if a preference actually changed before acting on it. + +## 5. Key Imports and Require Paths + +```lua +-- Base driver (for virtual/LAN devices) +local Driver = require "st.driver" + +-- Protocol-specific drivers +local ZigbeeDriver = require "st.zigbee" +local ZwaveDriver = require "st.zwave.driver" +local MatterDriver = require "st.matter.driver" + +-- Capabilities +local capabilities = require "st.capabilities" + +-- Zigbee defaults (pre-built handlers for common capabilities) +local defaults = require "st.zigbee.defaults" + +-- Zigbee clusters (for building commands/reading attributes) +local zcl_clusters = require "st.zigbee.zcl" + +-- Z-Wave command classes +local cc = require "st.zwave.CommandClass" +local SwitchBinary = require "st.zwave.CommandClass.SwitchBinary" + +-- Matter clusters +local clusters = require "st.matter.clusters" + +-- Utilities +local utils = require "st.utils" +local json = require "st.json" +local log = require "log" + +-- Coroutine runtime +local cosock = require "cosock" + +-- LAN utils +local socket = cosock.socket +local luncheon = require "luncheon" +local luxure = require "luxure" +local lustre = require "lustre" +``` + +### Zigbee driver example (from zigbee-switch) + +```lua +local capabilities = require "st.capabilities" +local ZigbeeDriver = require "st.zigbee" +local defaults = require "st.zigbee.defaults" + +local template = { + supported_capabilities = { + capabilities.switch, + capabilities.switchLevel, + capabilities.colorControl, + capabilities.colorTemperature, + }, + sub_drivers = require("sub_drivers"), + lifecycle_handlers = { + init = device_init, + added = device_added, + }, +} + +-- Register default Zigbee handlers for all supported capabilities +defaults.register_for_default_handlers(template, + template.supported_capabilities, + {native_capability_cmds_enabled = true, native_capability_attrs_enabled = true} +) + +local driver = ZigbeeDriver("zigbee_switch", template) +driver:run() +``` + +This pattern - declare supported capabilities, register defaults, add overrides via sub_drivers and lifecycle_handlers, then construct and run - is the standard structure +for all protocol-based Edge drivers. + +## 6. Default Handlers and Protocol-Specific Default Functionality + +When a driver declares `supported_capabilities` in its template, the framework automatically registers default handlers for each capability. The registration uses `or`-merge +logic: **driver-defined handlers always take precedence over defaults.** If the driver already registered a handler for a given cluster/attribute/command slot, the default +is silently skipped. + +Registration happens in `st.{zigbee,zwave,matter}.defaults.init.lua` via `register_for_default_handlers(driver, capabilities, opts)`: +1. Iterates `supported_capabilities` +2. For each capability, requires the corresponding defaults module +3. Merges `zigbee_handlers`, `zwave_handlers`, or `matter_handlers` (only where driver hasn't defined one) +4. Also merges `attribute_configurations` (Zigbee), `get_refresh_commands` (Z-Wave), or `subscribed_attributes` (Matter) + +### Zigbee specific default functionality + +The default `doConfigure` handler (`device_management.configure`): +1. Sends a `refresh` command (reads all configured attributes) +2. Calls `device:configure()` which iterates all configured attributes and for each: + - Sends a ZDO Bind Request + - Sends a Configure Reporting command with the attribute's min/max interval, data type, and reportable change +3. Also handles IAS Zone enrollment if the device supports cluster `0x0500` + +### Z-Wave specific default functionality + +doConfigure calls `device:default_configure()` which calls `device:refresh()`. The default refresh iterates `get_refresh_commands` from all default capability modules and sends Get commands for each supported CC. +Refresh collects `get_refresh_commands` from all default modules, sends Get commands + +### Matter specific default functionality + +TODO + +## 7. Unit Test Framework + +Load the `testing-edge-drivers` skill for details on the built in unit test framework for to test Zigbee, Z-Wave, and Matter drivers. + diff --git a/.agents/skills/understanding-profiles/SKILL.md b/.agents/skills/understanding-profiles/SKILL.md new file mode 100644 index 0000000000..0f169057e5 --- /dev/null +++ b/.agents/skills/understanding-profiles/SKILL.md @@ -0,0 +1,369 @@ +--- +name: understanding-profiles +description: Understanding and defining SmartThings capabilities, device profiles, preferences, and embedded device configurations for Edge Drivers +--- + +# SmartThings Capabilities, Profiles, and Preferences + +## 1. What Are Capabilities? + +Capabilities are the fundamental abstraction in SmartThings. They define what a device can do and what state it can report. Each capability consists of: + +- **Attributes**: State/status values (e.g., `switch` has attribute `switch` with values `on`/`off`) +- **Commands**: Actions that control the device (e.g., `on()`, `off()`, `setLevel(level)`) + +A capability definition specifies data types, units, and constraints for its attributes and commands. + +### Data Types +| Type | Example | Description | +|------|---------|-------------| +| string | `"locked"` | May have enum or pattern constraints | +| integer | `5` | Whole number, may have min/max | +| number | `5.5` | Fractional values allowed | +| boolean | `true` | true or false | +| object | `{x: 12}` | Map of name-value pairs | +| array | `["heat","cool"]` | List of single type | + +### Common Capabilities +- `switch` - on/off control +- `switchLevel` - dimming (0-100) +- `temperatureMeasurement` - temperature reading +- `battery` - battery percentage +- `contactSensor` - open/closed +- `motionSensor` - active/inactive +- `lock` - locked/unlocked +- `thermostatMode`, `thermostatHeatingSetpoint`, `thermostatCoolingSetpoint` +- `colorTemperature`, `colorControl` +- `refresh` - request device state update +- `firmwareUpdate` - OTA firmware management +- `healthCheck` - device connectivity monitoring + +Full reference: https://developer.smartthings.com/docs/devices/capabilities/capabilities-reference + +## 2. Standard vs Custom Capabilities + +### Standard Capabilities +Standard capabilities live under the `smartthings` namespace but are referenced without a namespace prefix: +```yaml +- id: switch + version: 1 +- id: temperatureMeasurement + version: 1 +``` + +### Custom Capabilities +Custom capabilities use the format `namespace.capabilityName`: +```yaml +- id: perfectlife6617.customGarageDoor + version: 1 +``` + +A namespace is auto-generated per developer account (e.g., `perfectlife6617`). Custom capabilities are created via the SmartThings CLI: +``` +smartthings capabilities:create -i capability.json +``` + +Custom capabilities require a Capability Presentation to render properly in the app. + +## 3. Device Profile YAML Format + +Device profiles define which capabilities a device exposes, organized into components. They live in `profiles/` directories within driver packages. + +### Basic Profile Example (from `zwave-lock`) +```yaml +name: base-lock +components: +- id: main + capabilities: + - id: lock + version: 1 + - id: lockCodes + version: 1 + - id: battery + version: 1 + - id: refresh + version: 1 + categories: + - name: SmartLock +``` + +### Multi-Component Profile (from `zigbee-fan`) +```yaml +name: fan-light +components: + - id: main + label: Fan + capabilities: + - id: switch + version: 1 + - id: fanSpeed + version: 1 + config: + values: + - key: "fanSpeed.value" + range: [0, 3] + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Fan + - id: light + label: Light + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [0, 100] + - id: refresh + version: 1 + categories: + - name: Light +``` + +### Profile with Embedded Config and Preferences (from `zigbee-contact`) +```yaml +name: multi-sensor +components: +- id: main + capabilities: + - id: contactSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: threeAxis + version: 1 + - id: accelerationSensor + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MultiFunctionalSensor +preferences: + - preferenceId: tempOffset + explicit: true + - preferenceId: certifiedpreferences.garageSensor + explicit: true +``` + +### Key Profile Rules +- Must have at least one component; the primary is always `id: main` +- Use multiple components when the same capability is needed more than once (e.g., multi-gang switch) +- Each component needs at least one capability +- `categories` determines the device icon in the app (e.g., `SmartLock`, `Fan`, `Light`, `Thermostat`, `MultiFunctionalSensor`) +- `version: 1` is always used (only version supported) + +## 4. Embedded Device Configurations + +Embedded device configs let you customize the SmartThings app UI directly in the profile YAML, without creating a separate Device Presentation. Only supported by Edge Drivers. + +### Range Constraint +```yaml +- id: colorTemperature + config: + values: + - key: "colorTemperature.value" + range: [2600, 6200] +``` + +### Enabled Values (filter enum options) +```yaml +- id: thermostatOperatingState + version: 1 + config: + values: + - key: "thermostatOperatingState.value" + enabledValues: + - heating + - cooling + - fan only + - idle +``` + +### Separate Attribute vs Command Values +```yaml +- id: thermostatMode + config: + values: + - key: thermostatMode.value + enabledValues: + - off + - heat + - eco + - key: setThermostatMode + enabledValues: + - off + - heat +``` + +### Enum Commands +```yaml +- id: alarm + config: + values: + - key: alarm.value + enabledValues: + - off + - siren + - key: "{{enumCommands}}" + enabledValues: + - off + - siren +``` + +When you package the driver, the platform auto-generates a Device Presentation from these configs. + +## 5. Preferences + +Preferences let users configure device behavior from Settings in the SmartThings app. + +### Two Types + +**Explicit (shared/reusable):** Defined externally, referenced by ID in the profile: +```yaml +preferences: + - preferenceId: tempOffset + explicit: true +``` + +Standard explicit preferences include: `tempOffset`, `humidityOffset`, `motionSensitivity`, `reportingInterval`, `reverse`, `presetPosition`, `username`, `password`. + +`tempOffset` and `humidityOffset` are automatically applied by the platform to attribute values - no driver code needed. + +**Embedded (inline in profile):** Defined directly in the profile YAML: +```yaml +preferences: + - title: "IP Address" + name: ipAddress + description: "IP address of the Pi-Hole" + required: true + preferenceType: string + definition: + minLength: 7 + maxLength: 15 + stringType: text + default: localhost +``` + +### Preference Types +| Type | Definition Fields | +|------|------------------| +| boolean | `default` | +| integer | `minimum`, `maximum`, `default` | +| number | `minimum`, `maximum`, `default` | +| string | `stringType` (text/paragraph/password), `minLength`, `maxLength`, `default` | +| enumeration | `options` (key-value map), `default` (must match a key) | + +### Accessing Preferences in Lua + +Query current value: +```lua +local offset = device.preferences.tempOffset +local level = command.args.level + device.preferences.levelOffset +``` + +Handle preference changes via `infoChanged` lifecycle: +```lua +local function device_info_changed(driver, device, event, args) + if args.old_st_store.preferences.sensitivityLevel ~= device.preferences.sensitivityLevel then + device:send() + end +end +``` + +For sleepy Z-Wave devices, use `device:set_update_preferences_fn(fn)` which fires on wakeup. + +## 6. config.yml + +The `config.yml` file is the driver package manifest. It lives at the root of each driver directory. + +```yaml +name: 'Zigbee Thermostat' +defaultProfile: 'thermostat-battery-powerSource' +packageKey: 'zigbee-thermostat' +permissions: + zigbee: {} +description: "SmartThings driver for Zigbee thermostat devices" +vendorSupportInformation: "https://support.smartthings.com" +``` + +### Fields +| Field | Description | +|-------|-------------| +| `name` | Human-readable driver name | +| `packageKey` | Unique package identifier | +| `permissions` | Protocol access: `zigbee: {}`, `zwave: {}`, `lan: {}`, `matter: {}` | +| `description` | Driver description | +| `defaultProfile` | Profile name used when no fingerprint match specifies one | +| `vendorSupportInformation` | Support URL | + +## 7. Fingerprints + +Fingerprints map physical devices to profiles. They live in `fingerprints.yml` at the driver root. + +### Zigbee Fingerprints +```yaml +zigbeeManufacturer: + - id: "LUMI/lumi.motion.ac02" + deviceLabel: Aqara Motion Sensor P1 + manufacturer: LUMI + model: lumi.motion.ac02 + deviceProfileName: motion-illuminance-battery-aqara + - id: "SmartThings/motionv5" + deviceLabel: Motion Sensor + manufacturer: SmartThings + model: motionv5 + deviceProfileName: motion-temp-battery + +zigbeeGeneric: + - id: kickstarter/motion/1 + deviceLabel: SmartThings Motion Sensor + zigbeeProfiles: + - 0xFC01 + deviceIdentifiers: + - 0x013A + deviceProfileName: smartsense-motion +``` + +### Key Fingerprint Fields +| Field | Description | +|-------|-------------| +| `id` | Unique identifier for the fingerprint | +| `deviceLabel` | Default label shown to users | +| `manufacturer` | Device manufacturer string | +| `model` | Device model string | +| `deviceProfileName` | Which profile from `profiles/` to use | +| `zigbeeProfiles` | (zigbeeGeneric) Zigbee profile IDs | +| `deviceIdentifiers` | (zigbeeGeneric) Zigbee device type IDs | + +Z-Wave fingerprints use `manufacturerId`, `productType`, and `productId` instead. + +## 8. Relationship: config.yml + Profiles + Fingerprints + +``` +driver/ +├── config.yml # Package manifest, declares defaultProfile +├── fingerprints.yml # Maps hardware → profile by deviceProfileName +├── profiles/ +│ ├── basic-device.yml # Profile A +│ └── advanced-device.yml # Profile B +└── src/ + └── init.lua # Driver logic +``` + +Flow: +1. A device joins the hub +2. The platform matches it against `fingerprints.yml` entries +3. The matched fingerprint's `deviceProfileName` selects which profile to use +4. If no fingerprint matches, `defaultProfile` from `config.yml` is used +5. The profile defines capabilities, components, categories, and preferences +6. Embedded `config` in the profile customizes the app UI +7. The driver's Lua code handles capability commands and emits attribute events diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..cf17ce46b3 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,52 @@ + You are working in the SmartThings Edge Drivers repository. Drivers are written in **Lua 5.3** and + run on SmartThings hubs. They translate Zigbee, Z-Wave, Matter, and LAN protocol messages into + SmartThings capability commands and events. + + For full context, read `AGENTS.md` at the repository root. It covers driver structure, lifecycle, + profiles, and available skills for deeper domain knowledge. + + ## Standard Commands + + ```bash + # Run tests + python3 tools/run_driver_tests.py -vv -f + + # Lint + luacheck --config .github/workflows/.luacheckrc + + # Deploy + smartthings edge:drivers:package --hub= --channel= +``` + +## Rules + +Always: + + - Run tests before considering a change complete + - Run luacheck on modified Lua files + - Use existing standard capabilities before creating custom ones + - Follow existing driver structure patterns + +Ask before: + + - Modifying device profile YAML files (changes affect production devices) + - Adding new custom capabilities + - Changing config.yml permissions + +Never: + + - Commit hardcoded API keys or tokens + - Skip tests for driver changes + - Use Lua features beyond 5.3 + +## Skills + +Load these files for deeper knowledge when working in each area: + +| Task | Skill file | +|------|-----------| +| Driver lifecycle, dispatch, default handlers | .agents/skills/understanding-lua-libraries/SKILL.md | +| Profiles, capabilities, preferences, fingerprints | .agents/skills/understanding-profiles/SKILL.md | +| Writing and running tests | .agents/skills/testing-edge-drivers/SKILL.md | +| Luacheck / code style | .agents/skills/linting-and-style/SKILL.md | +| Environment setup, deploying, sharing via channels | .agents/skills/dev-workflow/SKILL.md | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..ecb9478f81 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,130 @@ +# SmartThings Edge Drivers — Agent Instructions + +You are an expert Lua 5.3 engineer and SmartThings Edge Driver maintainer. This repository contains production Edge Drivers for the SmartThings platform, spanning Zigbee, Z-Wave, Matter, and LAN protocols. + +Lua drivers translate between device protocol messages and SmartThings capability commands/events to support hub connected devices on the platform. + +## Repository Structure + +``` +drivers/ # Edge Drivers organized by vendor (SmartThings/, Aqara/, etc.) + // + config.yml # Driver metadata, permissions, capabilities, preferences + profiles/ # Device profile YAML definitions + fingerprints.yml # Device identification fingerprints (optional, can be in Lua) + src/ + init.lua # Driver entry point + sub_drivers/ # Protocol/device-specific sub-drivers (optional) + test/ # Integration tests +lua_libs/ # SmartThings Lua runtime libraries (from latest GitHub release) +tools/ # Test runners, deploy scripts, utilities +.github/workflows/ # CI: tests, luacheck, packaging +``` + +## Standard Commands + +### Run Tests +```bash +python3 tools/run_driver_tests.py -vv -f +``` +The filter matches against driver directory/file names. Load the `testing-edge-drivers` skill for details. + +### Lint +```bash +luacheck --config .github/workflows/.luacheckrc +``` +Load the `linting-and-style` skill for configuration details and common fixes. + +### Deploy a Driver +```bash +smartthings edge:drivers:package --hub= --channel= +``` +Load the `dev-workflow` skill for channel setup and sharing instructions. + +## Driver Anatomy + +Drivers live under `drivers///`. The canonical layout is: + +``` +drivers/// + config.yml # Driver metadata: name, packageKey, permissions, description + fingerprints.yml # Device matching rules (Zigbee, Z-Wave, Matter only) + search-parameters.yml # SSDP/mDNS discovery hints (LAN drivers only) + profiles/ + .yml # One file per device profile + src/ + init.lua # Driver entry point; creates template and calls :run() + sub_drivers.lua # Optional: list of sub-driver require paths + / + init.lua # Sub-driver table: NAME, can_handle, handlers + can_handle.lua # Optional: separated device-matching function + fingerprints.lua # Optional: Lua-side fingerprint list for can_handle +``` + +Load the `understanding-lua-libraries` skill for detailed information on the driver framework. + +### Fingerprints (`fingerprints.yml`) + +Fingerprints tell the platform which driver to assign to a newly-joined device. +When a device is discovered, the hub reads its identifying properties and sends +them to the SmartThings cloud, which finds the best matching fingerprint and +installs the corresponding driver. + +Manufacturer-specific fingerprints always win over generic ones when both match. + +LAN drivers do **not** use `fingerprints.yml`. They define a `discovery` handler in the driver +template which is called when the hub forwards discovery requests to the driver. This discovery +handler is responsible for searching for the device on the network and creating the device. + +### Device Profiles (`profiles/*.yml`) + +A profile declares the SmartThings **capabilities** a device exposes, grouped into +**components**. The `main` component is the primary one. A fingerprint's +`deviceProfileName` value must exactly match the `name` field in a profile file. + +Load the `understanding-profiles` skill for details on profiles and how they +define devices on the platform. + +## Lua Libraries (`lua_libs/`) + +The `lua_libs/` directory at the repository root is setup by the developer and not committed +to the repository. It contains the SmartThings Edge SDK: the Lua framework, protocol libraries, +test utilities, and third-party dependencies. **This directory must be present for tests to run.** +Load the `dev-workflow` skill to help with initial setup. + +Load the `understanding-lua-libraries` skill for detailed information on the lua libraries. + +--- + +## Rules + +### ✅ ALWAYS +- Run tests before considering a change complete +- Run luacheck on modified Lua files +- Use existing capabilities from the SmartThings reference before creating custom ones +- Follow the existing driver structure patterns in this repo +- Use `require` paths relative to `src/` for driver code, and `lua_libs/` for library code + +### ⚠️ ASK FIRST +- Before modifying device profile YAML files (changes affect production devices) +- Before adding new custom capabilities +- Before changing `config.yml` permissions +- Before modifying shared library code in `lua_libs/` + +### 🚫 NEVER +- Commit hardcoded API keys, tokens, or hub UUIDs +- Modify files in `lua_libs/` (these come from upstream releases) +- Skip tests for driver changes +- Use Lua features beyond 5.3 (the hub runtime is Lua 5.3) + +## Available Skills + +Load these for deeper domain knowledge: + +| Skill | When to Use | Skill file | +|-------|-------------|------------| +| `understanding-profiles` | Defining or modifying capabilities, profiles, preferences, or device configurations | .agents/skills/understanding-profiles/SKILL.md | +| `understanding-lua-libraries` | Understanding the driver lifecycle, message dispatchers, default handlers, or protocol objects | .agents/skills/understanding-lua-libraries/SKILL.md | +| `testing-edge-drivers` | Running and writing driver tests using the integration test framework | .agents/skills/testing-edge-drivers/SKILL.md | +| `linting-and-style` | Running luacheck or fixing style issues | .agents/skills/linting-and-style/SKILL.md | +| `dev-workflow` | Setting up the dev environment, deploying drivers, or sharing via channels | .agents/skills/dev-workflow/SKILL.md | From 5e428adc3ecec3429b31667457fa18d7232a1c8e Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Mon, 18 May 2026 17:22:31 -0500 Subject: [PATCH 10/19] WWSTCERT-11638 Govee Floor Lamp 3 (#2972) * WWSTCERT-11638 Govee Floor Lamp 3 * Apply suggestion from @ctowns Co-authored-by: Cooper Towns --------- Co-authored-by: Cooper Towns --- .../SmartThings/matter-switch/fingerprints.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index b73ac0e98f..2eeb26b3d4 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -813,6 +813,21 @@ matterManufacturer: vendorId: 0x1387 productId: 0x1741 deviceProfileName: light-color-level + - id: "4999/5808" + deviceLabel: Govee Floor Lamp 3 + vendorId: 0x1387 + productId: 0x16B0 + deviceProfileName: light-color-level + - id: "4999/5824" + deviceLabel: Govee Floor Lamp 3 Lite + vendorId: 0x1387 + productId: 0x16C0 + deviceProfileName: light-color-level + - id: "4999/4720" + deviceLabel: Govee Ceiling Light Ultra (21inch) + vendorId: 0x1387 + productId: 0x1270 + deviceProfileName: light-color-level # Hager - id: "4741/8" deviceLabel: Hager matter 2 buttons (battery) From 4485f2b59dcbb5b11f95d3bef273888c50c5f1bd Mon Sep 17 00:00:00 2001 From: West Zhao <55424074+Oniums@users.noreply.github.com> Date: Tue, 19 May 2026 23:57:17 +0800 Subject: [PATCH 11/19] WWSTCERT-10701 Add Sonoff SNZB-01M Smart Scene Button into zigbee-button. (#2510) * Add Sonoff SNZB-01M Smart Scene Button into zigbee-button. * modify Copyright Date * modify test_sonoff.py fix PR error * Update copyright year from 2022 to 2026,remove remove whitespace-only lines Update copyright year from 2022 to 2026,remove remove whitespace-only lines * remove log.info remove log.info * Use defaults for Sonoff battery and added lifecycle - Remove custom battery attribute handler and added lifecycle in Sonoff subdriver - Align Sonoff tests with default added behavior and keep battery report coverage * remove log * Fix Sonoff multi-button sub-driver registration and test execution * update copyright sections for sonoff multi-button * fix: address review feedback for Sonoff SNZB-01M button support - Reuse existing four-buttons-battery profile instead of custom sonoff-buttons-battery - Mirror button events to main component via emit_event for device list visibility - Update test profile reference and add main component event assertions - Remove unused sonoff-buttons-battery.yml profile * fix: resolve Sonoff SNZB-01M unit test failures - replace unsupported build_custom_report_attribute usage with an explicit custom Zigbee attribute report builder - add missing main component expectations for the added lifecycle test - add min_api_version guards to Sonoff test cases - keep Sonoff button event assertions aligned with the four-buttons-battery profile behavior * Removed the SONOFF entry from the generic supported values table and moved its button metadata initialization into the SONOFF sub-driver. * Apply suggestion from @KKlimczukS Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com> * Apply suggestion from @KKlimczukS Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com> * Apply suggestion from @KKlimczukS Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com> * Apply suggestion from @KKlimczukS Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com> --------- Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com> --- .../zigbee-button/fingerprints.yml | 6 + .../src/test/test_sonoff_button.lua | 279 ++++++++++++++++++ .../src/zigbee-multi-button/fingerprints.lua | 1 + .../src/zigbee-multi-button/init.lua | 1 - .../zigbee-multi-button/sonoff/can_handle.lua | 16 + .../sonoff/fingerprints.lua | 8 + .../src/zigbee-multi-button/sonoff/init.lua | 63 ++++ .../src/zigbee-multi-button/sub_drivers.lua | 1 + 8 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua diff --git a/drivers/SmartThings/zigbee-button/fingerprints.yml b/drivers/SmartThings/zigbee-button/fingerprints.yml index 09e915321b..347fa818df 100644 --- a/drivers/SmartThings/zigbee-button/fingerprints.yml +++ b/drivers/SmartThings/zigbee-button/fingerprints.yml @@ -267,6 +267,12 @@ zigbeeManufacturer: manufacturer: WALL HERO model: ACL-401SCA4 deviceProfileName: thirty-buttons + # SONOFF + - id: "SONOFF/SNZB-01M" + deviceLabel: SNZB-01M + manufacturer: SONOFF + model: SNZB-01M + deviceProfileName: four-buttons-battery - id: "MultIR/MIR-SO100" deviceLabel: MultiIR Smart button MIR-SO100 manufacturer: MultIR diff --git a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua new file mode 100644 index 0000000000..22f76bc383 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua @@ -0,0 +1,279 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local zb_const = require "st.zigbee.constants" +local messages = require "st.zigbee.messages" +local data_types = require "st.zigbee.data_types" +local zcl_messages = require "st.zigbee.zcl" +local report_attr = require "st.zigbee.zcl.global_commands.report_attribute" + +local SONOFF_PRIVATE_BUTTON_CLUSTER = 0xFC12 +local SONOFF_PRIVATE_ATTR = 0x0000 + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("four-buttons-battery.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SONOFF", + model = "SNZB-01M", + server_clusters = { 0x0001, 0xFC12 } + }, + [2] = { + id = 2, + manufacturer = "SONOFF", + model = "SNZB-01M", + server_clusters = { 0x0001, 0xFC12 } + }, + [3] = { + id = 3, + manufacturer = "SONOFF", + model = "SNZB-01M", + server_clusters = { 0x0001, 0xFC12 } + }, + [4] = { + id = 4, + manufacturer = "SONOFF", + model = "SNZB-01M", + server_clusters = { 0x0001, 0xFC12 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function build_test_attr_report(device, endpoint, value) + local report_body = report_attr.ReportAttribute({ + report_attr.ReportAttributeAttributeRecord(SONOFF_PRIVATE_ATTR, data_types.Uint8.ID, value) + }) + local zclh = zcl_messages.ZclHeader({ + cmd = data_types.ZCLCommandId(report_body.ID) + }) + local addrh = messages.AddressHeader( + device:get_short_address(), + endpoint, + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + SONOFF_PRIVATE_BUTTON_CLUSTER + ) + local message_body = zcl_messages.ZclMessageBody({ + zcl_header = zclh, + zcl_body = report_body + }) + + return messages.ZigbeeMessageRx({ + address_header = addrh, + body = message_body + }) +end + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "added lifecycle event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.button.numberOfButtons({ value = 4 }, { visibility = { displayed = false } }) + ) + ) + + -- Check initial events for button 1 + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button1", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button1", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + + -- Check initial events for button 2 + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button2", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + + -- Check initial events for button 3 + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button3", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button3", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + + -- Check initial events for button 4 + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button4", + capabilities.button.supportedButtonValues({ "pushed", "double", "held", "pushed_3x" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "button4", + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false })) + ) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Button pushed message should generate event", + function() + -- 0xFC12, 0x0000, 0x01 = pushed + local attr_report = build_test_attr_report(mock_device, 1, data_types.Uint8(0x01)) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Button double message should generate event", + function() + -- 0xFC12, 0x0000, 0x02 = double + local attr_report = build_test_attr_report(mock_device, 1, data_types.Uint8(0x02)) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.double({ state_change = true })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.button.double({ state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Button held message should generate event", + function() + -- 0xFC12, 0x0000, 0x03 = held + local attr_report = build_test_attr_report(mock_device, 1, data_types.Uint8(0x03)) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.held({ state_change = true })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.button.held({ state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Button pushed_3x message should generate event", + function() + -- 0xFC12, 0x0000, 0x04 = pushed_3x + local attr_report = build_test_attr_report(mock_device, 1, data_types.Uint8(0x04)) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", capabilities.button.button.pushed_3x({ state_change = true })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.button.pushed_3x({ state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Button 2 pushed message should generate event on button2 component", + function() + -- Endpoint 2 test + local attr_report = build_test_attr_report(mock_device, 2, data_types.Uint8(0x01)) + + test.socket.zigbee:__queue_receive({ mock_device.id, attr_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button2", capabilities.button.button.pushed({ state_change = true })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Battery percentage report should generate event", + function() + local battery_report = clusters.PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 180) + + test.socket.zigbee:__queue_receive({ mock_device.id, battery_report }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.battery.battery(90)) + ) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() + diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua index cf3903152d..05ef834306 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/fingerprints.lua @@ -36,6 +36,7 @@ local ZIGBEE_MULTI_BUTTON_FINGERPRINTS = { { mfr = "Vimar", model = "RemoteControl_v1.0" }, { mfr = "Linxura", model = "Smart Controller" }, { mfr = "Linxura", model = "Aura Smart Button" }, + { mfr = "SONOFF", model = "SNZB-01M" }, { mfr = "zunzunbee", model = "SSWZ8T" } } diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua index 84dc2af26e..daaa24a8b6 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/init.lua @@ -8,7 +8,6 @@ local supported_values = require "zigbee-multi-button.supported_values" local button_utils = require "button_utils" - local function added_handler(self, device) local config = supported_values.get_device_parameters(device) for _, component in pairs(device.profile.components) do diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua new file mode 100644 index 0000000000..a1f689f4de --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/can_handle.lua @@ -0,0 +1,16 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function sonoff_can_handle(opts, driver, device, ...) + local fingerprints = require("zigbee-multi-button.sonoff.fingerprints") + + for _, fingerprint in ipairs(fingerprints) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("zigbee-multi-button.sonoff") + end + end + + return false +end + +return sonoff_can_handle diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua new file mode 100644 index 0000000000..efba976feb --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local SONOFF_FINGERPRINTS = { + { mfr = "SONOFF", model = "SNZB-01M" } +} + +return SONOFF_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua new file mode 100644 index 0000000000..41390b4d44 --- /dev/null +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sonoff/init.lua @@ -0,0 +1,63 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local button_utils = require "button_utils" + +local SONOFF_CLUSTER_ID = 0xFC12 +local SONOFF_ATTR_ID = 0x0000 +local SONOFF_SUPPORTED_BUTTON_VALUES = { "pushed", "double", "held", "pushed_3x" } +local SONOFF_NUMBER_OF_BUTTONS = 4 + +local EVENT_MAP = { + [0x01] = capabilities.button.button.pushed, + [0x02] = capabilities.button.button.double, + [0x03] = capabilities.button.button.held, + [0x04] = capabilities.button.button.pushed_3x +} + +local function added_handler(self, device) + for _, component in pairs(device.profile.components) do + local number_of_buttons = component.id == "main" and SONOFF_NUMBER_OF_BUTTONS or 1 + device:emit_component_event(component, + capabilities.button.supportedButtonValues(SONOFF_SUPPORTED_BUTTON_VALUES, { visibility = { displayed = false } })) + device:emit_component_event(component, + capabilities.button.numberOfButtons({ value = number_of_buttons }, { visibility = { displayed = false } })) + end + + button_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, + capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) +end + +local function sonoff_attr_handler(driver, device, value, zb_rx) + local attr_val = value.value + local endpoint = zb_rx.address_header.src_endpoint.value + local button_name = "button" .. tostring(endpoint) + local event_func = EVENT_MAP[attr_val] + if event_func then + local comp = device.profile.components[button_name] + if comp then + local event = event_func({ state_change = true }) + device:emit_component_event(comp, event) + device:emit_event(event) + end + end +end + +local sonoff_handler = { + NAME = "SONOFF Multi-Button Handler", + lifecycle_handlers = { + added = added_handler + }, + zigbee_handlers = { + attr = { + [SONOFF_CLUSTER_ID] = { + [SONOFF_ATTR_ID] = sonoff_attr_handler + } + } + }, + can_handle = require("zigbee-multi-button.sonoff.can_handle") +} + +return sonoff_handler + diff --git a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua index d8d3611ba3..bd7828c466 100644 --- a/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-button/src/zigbee-multi-button/sub_drivers.lua @@ -6,6 +6,7 @@ local sub_drivers = { lazy_load_if_possible("zigbee-multi-button.ikea"), lazy_load_if_possible("zigbee-multi-button.somfy"), lazy_load_if_possible("zigbee-multi-button.ecosmart"), + lazy_load_if_possible("zigbee-multi-button.sonoff"), lazy_load_if_possible("zigbee-multi-button.centralite"), lazy_load_if_possible("zigbee-multi-button.adurosmart"), lazy_load_if_possible("zigbee-multi-button.heiman"), From 7bdbb97422fc143a2cde9ad447a9b1bc625e1252 Mon Sep 17 00:00:00 2001 From: Steven Green Date: Tue, 19 May 2026 09:06:23 -0700 Subject: [PATCH 12/19] WWSTCERT-9844 Aqara Smart Lock U400 (#2805) --- drivers/SmartThings/matter-lock/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 0c12932a2b..4ac4fc25b4 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -20,6 +20,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x286A deviceProfileName: lock-user-pin + - id: "4447/10244" + deviceLabel: Aqara Smart Lock U400 + vendorId: 0x115F + productId: 0x2804 + deviceProfileName: lock #Eufy - id: "5427/1" deviceLabel: eufy Smart Lock E31 From e2d6a8e4bb412e81b700c28209b39d9aeb2ae988 Mon Sep 17 00:00:00 2001 From: Cooper Towns Date: Tue, 12 May 2026 13:45:12 -0500 Subject: [PATCH 13/19] Fix handling for cameras without per-zone sensitivity handling --- .../camera_handlers/attribute_handlers.lua | 3 +- .../camera_handlers/capability_handlers.lua | 13 +- .../src/test/test_matter_camera.lua | 185 ++++++++++++++++++ 3 files changed, 194 insertions(+), 7 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index b32b1dd55c..53b35712e7 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -417,7 +417,8 @@ function CameraAttributeHandlers.triggers_handler(driver, device, ib, response) augmentationDuration = trigger.augmentation_duration.value, maxDuration = trigger.max_duration.value, blindDuration = trigger.blind_duration.value, - sensitivity = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and trigger.sensitivity.value + sensitivity = camera_utils.feature_supported(device, clusters.ZoneManagement.ID, + clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and trigger.sensitivity.value or nil }) end device:emit_event_for_endpoint(ib, capabilities.zoneManagement.triggers(triggers)) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua index 443c83956b..a26afd0ca7 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua @@ -272,9 +272,11 @@ function CameraCapabilityHandlers.handle_remove_zone(driver, device, cmd) end function CameraCapabilityHandlers.handle_create_or_update_trigger(driver, device, cmd) + local per_zone_sensitivity_supported = camera_utils.feature_supported( + device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY + ) if not cmd.args.augmentationDuration or not cmd.args.maxDuration or not cmd.args.blindDuration or - (camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and - not cmd.args.sensitivity) then + (per_zone_sensitivity_supported and not cmd.args.sensitivity) then local triggers = device:get_latest_state( camera_fields.profile_components.main, capabilities.zoneManagement.ID, capabilities.zoneManagement.triggers.NAME ) or {} @@ -284,8 +286,7 @@ function CameraCapabilityHandlers.handle_create_or_update_trigger(driver, device if not cmd.args.augmentationDuration then cmd.args.augmentationDuration = v.augmentationDuration end if not cmd.args.maxDuration then cmd.args.maxDuration = v.maxDuration end if not cmd.args.blindDuration then cmd.args.blindDuration = v.blindDuration end - if camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) and - not cmd.args.sensitivity then + if per_zone_sensitivity_supported and not cmd.args.sensitivity then cmd.args.sensitivity = v.sensitivity end found_trigger = true @@ -306,7 +307,7 @@ function CameraCapabilityHandlers.handle_create_or_update_trigger(driver, device augmentation_duration = cmd.args.augmentationDuration, max_duration = cmd.args.maxDuration, blind_duration = cmd.args.blindDuration, - sensitivity = cmd.args.sensitivity + sensitivity = per_zone_sensitivity_supported and cmd.args.sensitivity or nil -- omit even if provided by client if per-zone sensitivity is not supported } ) )) @@ -320,7 +321,7 @@ end function CameraCapabilityHandlers.handle_set_sensitivity(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) if not camera_utils.feature_supported(device, clusters.ZoneManagement.ID, clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY) then - device:send(clusters.ZoneManagement.attributes.Sensitivity:write(device, endpoint_id, cmd.args.id)) + device:send(clusters.ZoneManagement.attributes.Sensitivity:write(device, endpoint_id, cmd.args.sensitivity)) else device.log.warn(string.format("Can't set global zone sensitivity setting, per zone sensitivity enabled.")) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index fceaa4ccf5..057bd3ae7f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -119,6 +119,45 @@ local mock_device = test.mock_device.build_test_matter_device({ } }) +local mock_device_no_per_zone_sensitivity = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = CAMERA_EP, + clusters = { + { + cluster_id = clusters.CameraAvStreamManagement.ID, + feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.ZoneManagement.ID, + feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.PushAvStreamTransport.ID, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x0142, device_type_revision = 1} -- Camera + } + } + } +}) + local subscribe_request local subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.AttributeList, @@ -169,6 +208,27 @@ end test.set_test_init_function(test_init) + +local subscribe_request_no_per_zone_sensitivity +local subscribed_attributes_no_per_zone_sensitivity = { + clusters.CameraAvStreamManagement.attributes.AttributeList, +} + +local function test_init_no_per_zone_sensitivity() + test.mock_device.add_test_device(mock_device_no_per_zone_sensitivity) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_per_zone_sensitivity.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_per_zone_sensitivity.id, "init" }) + subscribe_request_no_per_zone_sensitivity = subscribed_attributes_no_per_zone_sensitivity[1]:subscribe(mock_device_no_per_zone_sensitivity) + subscribe_request_no_per_zone_sensitivity:merge(cluster_base.subscribe(mock_device_no_per_zone_sensitivity, nil, camera_fields.CameraAVSMFeatureMapAttr.cluster, camera_fields.CameraAVSMFeatureMapAttr.ID)) + subscribe_request_no_per_zone_sensitivity:merge(cluster_base.subscribe(mock_device_no_per_zone_sensitivity, nil, camera_fields.ZoneManagementFeatureMapAttr.cluster, camera_fields.ZoneManagementFeatureMapAttr.ID)) + for i, attr in ipairs(subscribed_attributes_no_per_zone_sensitivity) do + if i > 1 then subscribe_request_no_per_zone_sensitivity:merge(attr:subscribe(mock_device_no_per_zone_sensitivity)) end + end + test.socket.matter:__expect_send({mock_device_no_per_zone_sensitivity.id, subscribe_request_no_per_zone_sensitivity}) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_per_zone_sensitivity.id, "doConfigure" }) + mock_device_no_per_zone_sensitivity:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local additional_subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, clusters.CameraAvStreamManagement.attributes.ImageRotation, @@ -349,6 +409,83 @@ local function update_device_profile() test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) end +local additional_subscribed_attributes_no_per_zone_sensitivity = { + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, + clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.Viewport, + clusters.CameraAvStreamManagement.attributes.AttributeList, + clusters.ZoneManagement.attributes.MaxZones, + clusters.ZoneManagement.attributes.Zones, + clusters.ZoneManagement.attributes.Triggers, + clusters.ZoneManagement.attributes.SensitivityMax, + clusters.ZoneManagement.attributes.Sensitivity, + clusters.ZoneManagement.events.ZoneTriggered, + clusters.ZoneManagement.events.ZoneStopped, + clusters.OnOff.attributes.OnOff, +} + +local function update_device_profile_no_per_zone_sensitivity() + local expected_metadata = { + optional_component_capabilities = { + { + "main", + { + "videoCapture2", + "cameraViewportSettings", + "videoStreamSettings", + "zoneManagement", + } + }, + { + "statusLed", + { + "switch", + "mode" + } + } + }, + profile = "camera" +} + + test.socket.matter:__queue_receive({ + mock_device_no_per_zone_sensitivity.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device_no_per_zone_sensitivity, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + mock_device_no_per_zone_sensitivity:expect_metadata_update(expected_metadata) + test.wait_for_events() + local updated_device_profile = t_utils.get_profile_definition( + "camera.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device_no_per_zone_sensitivity:generate_info_changed({ profile = updated_device_profile })) + + test.socket.capability:__expect_send( + mock_device_no_per_zone_sensitivity:generate_test_message("main", capabilities.zoneManagement.supportedFeatures( + {"triggerAugmentation"} + )) + ) + + test.socket.capability:__expect_send( + mock_device_no_per_zone_sensitivity:generate_test_message("main", capabilities.videoStreamSettings.supportedFeatures( + {"liveStreaming", "clipRecording", "perStreamViewports"} + )) + ) + + for _, attr in ipairs(additional_subscribed_attributes_no_per_zone_sensitivity) do + subscribe_request_no_per_zone_sensitivity:merge(attr:subscribe(mock_device_no_per_zone_sensitivity)) + end + test.socket.matter:__expect_send({mock_device_no_per_zone_sensitivity.id, subscribe_request_no_per_zone_sensitivity}) +end + -- Matter Handler UTs test.register_coroutine_test( @@ -3008,5 +3145,53 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Zone Management trigger reports should omit sensitivity when per-zone sensitivity is unsupported, even if provided by client", + function() + update_device_profile_no_per_zone_sensitivity() + test.wait_for_events() + -- Create a trigger with + test.socket.capability:__queue_receive({ + mock_device_no_per_zone_sensitivity.id, + { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { + 1, 10, 3, 15, 3, 5 + }} + }) + test.socket.matter:__expect_send({ + mock_device_no_per_zone_sensitivity.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device_no_per_zone_sensitivity, CAMERA_EP, { + zone_id = 1, + initial_duration = 10, + augmentation_duration = 3, + max_duration = 15, + blind_duration = 3 + }) + }) + end, + { + test_init = test_init_no_per_zone_sensitivity, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Zone Management setSensitivity command should handle global sensitivity when per-zone sensitivity is unsupported", + function() + update_device_profile_no_per_zone_sensitivity() + test.wait_for_events() + test.socket.capability:__queue_receive({ + mock_device_no_per_zone_sensitivity.id, + { capability = "zoneManagement", component = "main", command = "setSensitivity", args = { 5 } } + }) + test.socket.matter:__expect_send({ + mock_device_no_per_zone_sensitivity.id, + clusters.ZoneManagement.attributes.Sensitivity:write(mock_device_no_per_zone_sensitivity, CAMERA_EP, 5) + }) + end, + { + test_init = test_init_no_per_zone_sensitivity, + min_api_version = 17 + } +) + -- run the tests test.run_registered_tests() From 8c3cf23a64a4c0a075e9b0a25db19b9a4378761f Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Tue, 19 May 2026 12:51:20 -0500 Subject: [PATCH 14/19] WWSTCERT-11708 Govee Ceiling Light Pro (#2984) --- drivers/SmartThings/matter-switch/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 2eeb26b3d4..20be11f9d1 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -674,7 +674,7 @@ matterManufacturer: productId: 0x61B9 deviceProfileName: light-color-level - id: "4999/24742" - deviceLabel: Govee Ceiling Light Pro (15 inch) + deviceLabel: Govee Ceiling Light Pro vendorId: 0x1387 productId: 0x60A6 deviceProfileName: light-color-level From 66d9df4198279e4db72f97262676263882510213 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Tue, 19 May 2026 12:53:23 -0500 Subject: [PATCH 15/19] WWSTCERT-11719 Linkind Smart Light Bulb (#2985) --- .../matter-switch/fingerprints.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 20be11f9d1..ede868ac0f 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -186,6 +186,26 @@ matterManufacturer: vendorId: 0x1396 productId: 0x10B1 deviceProfileName: light-level-colorTemperature + - id: "5014/4164" + deviceLabel: Linkind Smart Light Bulb + vendorId: 0x1396 + productId: 0x1044 + deviceProfileName: light-color-level + - id: "5014/4118" + deviceLabel: Linkind Smart Filament Bulb + vendorId: 0x1396 + productId: 0x1016 + deviceProfileName: light-color-level + - id: "5014/4116" + deviceLabel: Linkind Smart Light Bulb + vendorId: 0x1396 + productId: 0x1014 + deviceProfileName: light-color-level + - id: "5014/4165" + deviceLabel: Linkind Smart Downlight + vendorId: 0x1396 + productId: 0x1045 + deviceProfileName: light-color-level #Bosch Smart Home - id: "4617/12310" deviceLabel: Plug Compact [M] From 8b1cecd9f8d1653cbd6c0a61b31db8f451a53251 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 19 May 2026 16:33:17 -0500 Subject: [PATCH 16/19] Matter Switch: remove ghost events, provision after driver switch (#2978) --- drivers/SmartThings/matter-switch/src/init.lua | 1 + .../matter-switch/src/sub_drivers/aqara_cube/init.lua | 4 +++- .../matter-switch/src/sub_drivers/camera/init.lua | 1 + .../matter-switch/src/sub_drivers/eve_energy/init.lua | 4 +++- .../src/sub_drivers/ikea_scroll/init.lua | 1 + .../ikea_scroll/scroll_utils/device_configuration.lua | 1 - .../src/sub_drivers/third_reality_mk1/init.lua | 1 - .../src/switch_utils/device_configuration.lua | 1 - .../src/test/test_aqara_climate_sensor_w100.lua | 3 --- .../src/test/test_aqara_light_switch_h2.lua | 8 -------- .../matter-switch/src/test/test_ikea_scroll.lua | 4 ---- .../matter-switch/src/test/test_matter_button.lua | 1 - .../matter-switch/src/test/test_matter_camera.lua | 2 -- .../src/test/test_matter_multi_button.lua | 9 --------- .../src/test/test_matter_multi_button_motion.lua | 11 ----------- .../src/test/test_matter_multi_button_switch_mcd.lua | 10 +--------- .../matter-switch/src/test/test_third_reality_mk1.lua | 1 - 17 files changed, 10 insertions(+), 53 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index dea56b4a21..e2a04e103a 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -61,6 +61,7 @@ function SwitchLifecycleHandlers.driver_switched(driver, device) if device.network_type == device_lib.NETWORK_TYPE_MATTER and not switch_utils.detect_bridge(device) then device_cfg.match_profile(driver, device) end + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end function SwitchLifecycleHandlers.info_changed(driver, device, event, args) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua index 4e6b729fa3..ca9dfa76cf 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/aqara_cube/init.lua @@ -184,7 +184,9 @@ end local function do_configure(driver, device) end -- override driver_switched to prevent it running in the main driver -local function driver_switched(driver, device) end +local function driver_switched(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) +end local function initial_press_event_handler(driver, device, ib, response) if get_field_for_endpoint(device, INITIAL_PRESS_ONLY, ib.endpoint_id) then diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua index a72aa0b234..2aa698a08a 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/init.lua @@ -43,6 +43,7 @@ function CameraLifecycleHandlers.driver_switched(driver, device) if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0 then camera_cfg.match_profile(device) end + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end function CameraLifecycleHandlers.info_changed(driver, device, event, args) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua index 8222fc1378..59f0cbb9c2 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/eve_energy/init.lua @@ -246,7 +246,9 @@ end local function do_configure(driver, device) end -- override driver_switched to prevent it running in the main driver -local function driver_switched(driver, device) end +local function driver_switched(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) +end local function handle_refresh(self, device) requestData(device) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua index 32b23ccaae..6d6410dffa 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/init.lua @@ -25,6 +25,7 @@ end function IkeaScrollLifecycleHandlers.driver_switched(driver, device) scroll_cfg.match_profile(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end function IkeaScrollLifecycleHandlers.info_changed(driver, device, event, args) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua index 457804d495..0ad8a68826 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/device_configuration.lua @@ -22,7 +22,6 @@ function IkeaScrollConfiguration.configure_buttons(device) for _, ep in ipairs(scroll_fields.ENDPOINTS_PUSH) do device:send(clusters.Switch.attributes.MultiPressMax:read(device, ep)) switch_utils.set_field_for_endpoint(device, switch_fields.SUPPORTS_MULTI_PRESS, ep, true, {persist = true}) - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) end for _, ep in ipairs(scroll_fields.ENDPOINTS_UP_SCROLL) do -- and by extension, ENDPOINTS_DOWN_SCROLL device:emit_event_for_endpoint(ep, capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}})) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua index 1572886089..9c80c247be 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/init.lua @@ -36,7 +36,6 @@ local function configure_buttons(device) device.log.info(string.format("Configuring Supported Values for generic switch endpoint %d", ep)) local supportedButtonValues_event = capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}) device:emit_event_for_endpoint(ep, supportedButtonValues_event) - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) else device.log.info(string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) end diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua index 085de2d351..f757a41871 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -184,7 +184,6 @@ function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep if supportedButtonValues_event then device:emit_event_for_endpoint(ep, supportedButtonValues_event) end - device:emit_event_for_endpoint(ep, capabilities.button.button.pushed({state_change = false})) else device.log.info_with({hub_logs=true}, string.format("Component not found for generic switch endpoint %d. Skipping Supported Value configuration", ep)) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua index 1e23ffff4a..50713626f2 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua @@ -97,13 +97,10 @@ local aqara_mock_device = test.mock_device.build_test_matter_device({ local function configure_buttons() test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 3)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button1", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 4)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.pushed({state_change = false}))) test.socket.matter:__expect_send({aqara_mock_device.id, clusters.Switch.attributes.MultiPressMax:read(aqara_mock_device, 5)}) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.button.pushed({state_change = false}))) end local function test_init() diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua index 345afd08ce..f763cc769f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua @@ -7,7 +7,6 @@ local capabilities = require "st.capabilities" local utils = require "st.utils" local dkjson = require "dkjson" local clusters = require "st.matter.clusters" -local button_attr = capabilities.button.button local version = require "version" if version.api < 11 then @@ -147,16 +146,9 @@ local cumulative_report_val_39 = { local function configure_buttons() test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button4", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) end local function test_init() diff --git a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua index 5a33424e1f..806b25f33f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -156,13 +156,9 @@ local function ikea_scroll_subscribe() end local function expect_configure_buttons() - local button_attr = capabilities.button.button test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 3)}) - test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", button_attr.pushed({state_change = false}))) test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 6)}) - test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", button_attr.pushed({state_change = false}))) test.socket.matter:__expect_send({mock_ikea_scroll.id, clusters.Switch.attributes.MultiPressMax:read(mock_ikea_scroll, 9)}) - test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", button_attr.pushed({state_change = false}))) test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", capabilities.knob.supportedAttributes({"rotateAmount"}, {visibility = {displayed = false}}))) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua index 775f391e8e..061f2e2f47 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -49,7 +49,6 @@ local expected_initial_press_only_state = true local function expect_configure_button(device) test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(device:generate_test_message("main", button_attr.pushed({state_change = false}))) end local function test_init_for_lifecycle_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index 057bd3ae7f..9198dc253f 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -406,7 +406,6 @@ local function update_device_profile() end test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) end local additional_subscribed_attributes_no_per_zone_sensitivity = { @@ -3138,7 +3137,6 @@ test.register_coroutine_test( } mock_device:expect_metadata_update(updated_expected_metadata) test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("doorbell", capabilities.button.button.pushed({state_change = false}))) end, { min_api_version = 17 diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua index 2392800019..d7d5efde97 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -179,19 +179,10 @@ local mock_device_battery = test.mock_device.build_test_matter_device( local function expect_configure_buttons(device) test.socket.capability:__expect_send(device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, 50)}) - test.socket.capability:__expect_send(device:generate_test_message("button4", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({device.id, clusters.Switch.attributes.MultiPressMax:read(device, 60)}) - test.socket.capability:__expect_send(device:generate_test_message("button5", button_attr.pushed({state_change = false}))) end local function update_profile() diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua index 5500eca24e..44ee11861e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua @@ -125,22 +125,11 @@ local CLUSTER_SUBSCRIBE_LIST ={ local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button4", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 50)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.pushed({state_change = false}))) - test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, 60)}) - test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.pushed({state_change = false}))) end -- All messages queued and expectations set are done before the driver is actually run diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index 1c7cccea48..d469f72036 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -198,13 +198,8 @@ local CLUSTER_SUBSCRIBE_LIST_WITH_CHILD ={ local function expect_configure_buttons() test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button1", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = false}))) end local function test_init() @@ -422,7 +417,6 @@ test.register_coroutine_test( parent_assigned_child_key = string.format("%d", 7) }) test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) unsup_mock_device:expect_metadata_update({ profile = "2-button" }) unsup_mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) @@ -446,10 +440,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({unsup_mock_device.id, subscribe_request}) test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("main", button_attr.pushed({state_change = false}))) - test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed", "held"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(unsup_mock_device:generate_test_message("button2", button_attr.pushed({state_change = false}))) end, { test_init = test_init_mcd_unsupported_switch_device_type, @@ -471,6 +462,7 @@ test.register_coroutine_test( mock_child:expect_metadata_update({ profile = "light-color-level" }) mock_device:expect_metadata_update({ profile = "light-level-3-button" }) expect_configure_buttons() + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { min_api_version = 17 diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua index 82a8d4d641..b7e94797f6 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua @@ -185,7 +185,6 @@ local function configure_buttons() local component = "F" .. key if key == 1 then component = "main" end test.socket.capability:__expect_send(mock_device:generate_test_message(component, capabilities.button.supportedButtonValues({"pushed"}, {visibility = {displayed = false}}))) - test.socket.capability:__expect_send(mock_device:generate_test_message(component, capabilities.button.button.pushed({state_change = false}))) end end From f66648955bbaf0f68f977d37611079426c720cfc Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 19 May 2026 16:33:28 -0500 Subject: [PATCH 17/19] Provision driver switch in Matter Sensor (#2981) --- .../sub_drivers/air_quality_sensor/init.lua | 1 + ...test_matter_air_quality_sensor_modular.lua | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua index e8d0cd4701..383af643a6 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua @@ -53,6 +53,7 @@ function AirQualitySensorLifecycleHandlers.driver_switched(driver, device) local legacy_device_cfg = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.legacy_device_configuration" legacy_device_cfg.match_profile(device) end + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end function AirQualitySensorLifecycleHandlers.device_init(driver, device) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua index 9525992eb3..27921e0dec 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua @@ -334,6 +334,28 @@ local function test_aqs_device_type_update_modular_profile(generic_mock_device, test.socket.matter:__expect_send({generic_mock_device.id, subscribe_request}) end +test.register_coroutine_test( + "Handle driverSwitched event", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_modular_fingerprint.id, "driverSwitched" }) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.CarbonMonoxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.CarbonDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.NitrogenDioxideConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.OzoneConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.FormaldehydeConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.Pm1ConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.Pm25ConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.Pm10ConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.RadonConcentrationMeasurement.attributes.MeasurementUnit:read()}) + test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, clusters.TotalVolatileOrganicCompoundsConcentrationMeasurement.attributes.MeasurementUnit:read()}) + mock_device_modular_fingerprint:expect_metadata_update({ profile = "aqs-modular", optional_component_capabilities = {{"main", {"tvocMeasurement"}}} }) + mock_device_modular_fingerprint:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + test_init = test_init_modular_fingerprint, + } +) + test.register_coroutine_test( "Device with modular profile should enable correct optional capabilities - all clusters", function() From cb154a8522e79e99a1b08dbc64b2992af7f17b39 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 19 May 2026 16:33:39 -0500 Subject: [PATCH 18/19] Provision driver switch in Matter RVC (#2980) --- drivers/SmartThings/matter-rvc/src/init.lua | 1 + .../matter-rvc/src/test/test_matter_rvc.lua | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/drivers/SmartThings/matter-rvc/src/init.lua b/drivers/SmartThings/matter-rvc/src/init.lua index 1e52f321ee..2badac1413 100644 --- a/drivers/SmartThings/matter-rvc/src/init.lua +++ b/drivers/SmartThings/matter-rvc/src/init.lua @@ -120,6 +120,7 @@ local function driver_switched(driver, device) match_profile(driver, device) device:set_field(SERVICE_AREA_PROFILED, true, { persist = true }) device:send(clusters.RvcOperationalState.attributes.AcceptedCommandList:read()) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end local function info_changed(driver, device, event, args) diff --git a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua index 3b03b1a2e1..626f7fea60 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -218,6 +218,19 @@ local function operating_state_init() ) end +test.register_coroutine_test( + "Handle driverSwitched event", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) + test.socket.matter:__expect_send({mock_device.id, clusters.RvcOperationalState.attributes.AcceptedCommandList:read()}) + mock_device:expect_metadata_update({ profile = "rvc-clean-mode-service-area" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Assert profile applied over doConfigure", function() From 3015acec135b1938d18c7345865c4355207b438c Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Tue, 19 May 2026 16:33:48 -0500 Subject: [PATCH 19/19] Provision driver switch in Matter Lock (#2979) --- .../matter-lock/src/new-matter-lock/init.lua | 1 + .../src/test/test_new_matter_lock.lua | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua index adcbc04283..670b9dc63b 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua @@ -400,6 +400,7 @@ end local function driver_switched(driver, device) match_profile(driver, device, false) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end -- Matter Handler diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua index defeafe8e9..0b7535223b 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua @@ -82,6 +82,22 @@ end test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Handle driverSwitched event", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-user-pin-schedule" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Handle received OperatingMode(Normal, Vacation) from Matter device.", function()