From 7c945d02660da1e6010562e65f680f2d6e9906a6 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:53:12 +0800 Subject: [PATCH 01/24] add firstled-io_M2S2BAC (#3054) --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 3 ++- tools/localizations/cn.csv | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 1d67094618..b7a6852a7d 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/M2S2BAC" + deviceLabel: Mirror Series 2x2 1 + manufacturer: FIRSTLED + model: M2S2BAC + deviceProfileName: switch-light-restore-wireless - id: "ManDai Technology/DC2DC12MiV1" deviceLabel: Easy-Fit Driver manufacturer: ManDai Technology diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 2586782642..9ae9dfa66c 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -5,5 +5,6 @@ --The number of buttons determines how many buttons devices will be created. --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { - { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" } + { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "M2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" } } diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 007f8059b5..ab18642a6a 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,4 +141,5 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Mirror Series 2x2 1",镜系列2x2 1 "WALLHERO Thermostat ACL-403STC1",墙壁侠温控ACL-403STC1 From 0bc3f6d68662ef09f3e75095c4b024041739f25f Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:04:33 +0800 Subject: [PATCH 02/24] add firstled-io_M1S1BAC (#3055) * add firstled-io_M1S1BAC * Fix duplicate entry for FIRSTLED M1S1BAC model --------- Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index b7a6852a7d..80f3e79740 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,10 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/M1S1BAC" + deviceLabel: Mirror Series 1x1 1 + manufacturer: FIRSTLED + model: M1S1BAC - id: "FIRSTLED/M2S2BAC" deviceLabel: Mirror Series 2x2 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 9ae9dfa66c..0ad15cbd1c 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,5 +6,6 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "M1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "M2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" } } diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index ab18642a6a..be1ffa7247 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,5 +141,6 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Mirror Series 1x1 1",镜系列1x1 1 "Mirror Series 2x2 1",镜系列2x2 1 "WALLHERO Thermostat ACL-403STC1",墙壁侠温控ACL-403STC1 From eb2f3b69fb02848e1084f42d529a92e67a9b5dc0 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:17:24 +0800 Subject: [PATCH 03/24] add firstled-io_DL1S1BAC (#3056) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 80f3e79740..8fdd8850f4 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,10 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/DL1S1BAC" + deviceLabel: Dynamic Lite Series 1x1 1 + manufacturer: FIRSTLED + model: DL1S1BAC - id: "FIRSTLED/M1S1BAC" deviceLabel: Mirror Series 1x1 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 0ad15cbd1c..49ceca6976 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "DL1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "M1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "M2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" } } diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index be1ffa7247..d25b167bfd 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Dynamic Lite Series 1x1 1",灵动L系列1x1 1 "Mirror Series 1x1 1",镜系列1x1 1 "Mirror Series 2x2 1",镜系列2x2 1 "WALLHERO Thermostat ACL-403STC1",墙壁侠温控ACL-403STC1 From e6cc489f1e990e617aab6699d9f9cb2802578501 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:23:59 +0800 Subject: [PATCH 04/24] add firstled-io_DL2S2BAC (#3057) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 8fdd8850f4..39ae5dcf8a 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,10 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/DL2S2BAC" + deviceLabel: Dynamic Lite Series 2x2 1 + manufacturer: FIRSTLED + model: DL2S2BAC - id: "FIRSTLED/DL1S1BAC" deviceLabel: Dynamic Lite Series 1x1 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 49ceca6976..024d232795 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "DL2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "M1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "M2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" } diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index d25b167bfd..5131fc25ff 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Dynamic Lite Series 2x2 1",灵动L系列2x2 1 "Dynamic Lite Series 1x1 1",灵动L系列1x1 1 "Mirror Series 1x1 1",镜系列1x1 1 "Mirror Series 2x2 1",镜系列2x2 1 From 22ed34576392815f648b612422a51b57dd668186 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:38:41 +0800 Subject: [PATCH 05/24] add firstled-io_DL3S3BAC (#3058) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 39ae5dcf8a..9c949efd30 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,10 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/DL3S3BAC" + deviceLabel: Dynamic Lite Series 3x3 1 + manufacturer: FIRSTLED + model: DL3S3BAC - id: "FIRSTLED/DL2S2BAC" deviceLabel: Dynamic Lite Series 2x2 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 024d232795..2cbe62735c 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "DL3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "M1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 5131fc25ff..5e136800b6 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Dynamic Lite Series 3x3 1",灵动L系列3x3 1 "Dynamic Lite Series 2x2 1",灵动L系列2x2 1 "Dynamic Lite Series 1x1 1",灵动L系列1x1 1 "Mirror Series 1x1 1",镜系列1x1 1 From 795d63ae0a8f813d771f7f3faa1b1dba98f47a28 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 06:48:07 +0800 Subject: [PATCH 06/24] add firstled-io_DL4S4BAC (#3059) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 9c949efd30..4f90d9bac1 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,10 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/DL4S4BAC" + deviceLabel: Dynamic Lite Series 4x4 1 + manufacturer: FIRSTLED + model: DL4S4BAC - id: "FIRSTLED/DL3S3BAC" deviceLabel: Dynamic Lite Series 3x3 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 2cbe62735c..c29f174f99 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "DL4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 5e136800b6..d5b3024c22 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Dynamic Lite Series 4x4 1",灵动L系列4x4 1 "Dynamic Lite Series 3x3 1",灵动L系列3x3 1 "Dynamic Lite Series 2x2 1",灵动L系列2x2 1 "Dynamic Lite Series 1x1 1",灵动L系列1x1 1 From d132e869786897ebe3f328cdf17430f1d8298622 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:03:42 +0800 Subject: [PATCH 07/24] add firstled-io_G1S1BAC (#3060) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 4f90d9bac1..885b084409 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,10 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/G1S1BAC" + deviceLabel: Guanzhi Series 1x1 1 + manufacturer: FIRSTLED + model: G1S1BAC - id: "FIRSTLED/DL4S4BAC" deviceLabel: Dynamic Lite Series 4x4 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index c29f174f99..7ac6eaf468 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "G1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index d5b3024c22..ef3ff55900 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Guanzhi Series 1x1 1",观致系列1x1 1 "Dynamic Lite Series 4x4 1",灵动L系列4x4 1 "Dynamic Lite Series 3x3 1",灵动L系列3x3 1 "Dynamic Lite Series 2x2 1",灵动L系列2x2 1 From 4320d7442024a6685e7eb3dc4a2a5db22adb899d Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:11:30 +0800 Subject: [PATCH 08/24] add firstled-io_G2S2BAC (#3061) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 885b084409..fc69dcccd6 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,10 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/G2S2BAC" + deviceLabel: Guanzhi Series 2x2 1 + manufacturer: FIRSTLED + model: G2S2BAC - id: "FIRSTLED/G1S1BAC" deviceLabel: Guanzhi Series 1x1 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 7ac6eaf468..b19894cf1e 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "G2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index ef3ff55900..c17d0042f8 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Guanzhi Series 2x2 1",观致系列2x2 1 "Guanzhi Series 1x1 1",观致系列1x1 1 "Dynamic Lite Series 4x4 1",灵动L系列4x4 1 "Dynamic Lite Series 3x3 1",灵动L系列3x3 1 From 1fbeb0ead6391167217780917602416b94392f2e Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:27:08 +0800 Subject: [PATCH 09/24] add firstled-io_G3S3BAC (#3062) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 14 +++++++++++++- .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index fc69dcccd6..fe5963dd64 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,34 +2457,46 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/G3S3BAC" + deviceLabel: Guanzhi Series 3x3 1 + manufacturer: FIRSTLED + model: G3S3BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/G2S2BAC" deviceLabel: Guanzhi Series 2x2 1 manufacturer: FIRSTLED model: G2S2BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/G1S1BAC" deviceLabel: Guanzhi Series 1x1 1 manufacturer: FIRSTLED model: G1S1BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/DL4S4BAC" deviceLabel: Dynamic Lite Series 4x4 1 manufacturer: FIRSTLED model: DL4S4BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/DL3S3BAC" deviceLabel: Dynamic Lite Series 3x3 1 manufacturer: FIRSTLED model: DL3S3BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/DL2S2BAC" deviceLabel: Dynamic Lite Series 2x2 1 manufacturer: FIRSTLED model: DL2S2BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/DL1S1BAC" deviceLabel: Dynamic Lite Series 1x1 1 manufacturer: FIRSTLED model: DL1S1BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/M1S1BAC" deviceLabel: Mirror Series 1x1 1 manufacturer: FIRSTLED model: M1S1BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/M2S2BAC" deviceLabel: Mirror Series 2x2 1 manufacturer: FIRSTLED @@ -2574,4 +2586,4 @@ zigbeeGeneric: - 0x0006 #OnOff - 0x0702 #Simple Metering - 0x0B04 #Electrical Measurment - deviceProfileName: switch-power \ No newline at end of file + deviceProfileName: switch-power diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index b19894cf1e..672ffd749d 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "G3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "DL4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index c17d0042f8..7560f4506d 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Guanzhi Series 3x3 1",观致系列3x3 1 "Guanzhi Series 2x2 1",观致系列2x2 1 "Guanzhi Series 1x1 1",观致系列1x1 1 "Dynamic Lite Series 4x4 1",灵动L系列4x4 1 From 1a4b25804b180e9d186be7c5817e240f91a1f3b4 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:43:26 +0800 Subject: [PATCH 10/24] add firstled-io_MX1S2BAC (#3063) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index fe5963dd64..4fec9c6290 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/MX1S2BAC" + deviceLabel: Matrix X Series 1x2 1 + manufacturer: FIRSTLED + model: MX1S2BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/G3S3BAC" deviceLabel: Guanzhi Series 3x3 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 672ffd749d..95705deff9 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "MX1S2BAC", children = 1, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 7560f4506d..257b486449 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Matrix X Series 1x2 1",矩阵系列1x2 1 "Guanzhi Series 3x3 1",观致系列3x3 1 "Guanzhi Series 2x2 1",观致系列2x2 1 "Guanzhi Series 1x1 1",观致系列1x1 1 From 4f1da8316872c02647c73234f0d6bb04c9df41e1 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:49:26 +0800 Subject: [PATCH 11/24] add firstled-io_MX2S2BAC (#3064) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 4fec9c6290..293c311524 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/MX2S2BAC" + deviceLabel: Matrix X Series 2x2 1 + manufacturer: FIRSTLED + model: MX2S2BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/MX1S2BAC" deviceLabel: Matrix X Series 1x2 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 95705deff9..68593900ac 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "MX2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX1S2BAC", children = 1, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 257b486449..91d05a1b4f 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Matrix X Series 2x2 1",矩阵系列2x2 1 "Matrix X Series 1x2 1",矩阵系列1x2 1 "Guanzhi Series 3x3 1",观致系列3x3 1 "Guanzhi Series 2x2 1",观致系列2x2 1 From f884d830b3e7ef93e2dc9b5037e4679319883c98 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 19:54:45 +0800 Subject: [PATCH 12/24] add firstled-io_MX3S2BAC (#3065) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 293c311524..61b139e873 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/MX3S2BAC" + deviceLabel: Matrix X Series 3x2 1 + manufacturer: FIRSTLED + model: MX3S2BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/MX2S2BAC" deviceLabel: Matrix X Series 2x2 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 68593900ac..361637739b 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "MX3S2BAC", children = 3, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX1S2BAC", children = 1, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "G3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 91d05a1b4f..4cd241d156 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Matrix X Series 3x2 1",矩阵系列3x2 1 "Matrix X Series 2x2 1",矩阵系列2x2 1 "Matrix X Series 1x2 1",矩阵系列1x2 1 "Guanzhi Series 3x3 1",观致系列3x3 1 From a46cfc27cd2007f4ed95f462e06fd2fc4d76ffe7 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:01:56 +0800 Subject: [PATCH 13/24] add firstled-io_MX4S2BAC (#3066) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 61b139e873..a6b565216f 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/MX4S2BAC" + deviceLabel: Matrix X Series 4x2 1 + manufacturer: FIRSTLED + model: MX4S2BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/MX3S2BAC" deviceLabel: Matrix X Series 3x2 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 361637739b..1db3711245 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "MX4S2BAC", children = 4, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX3S2BAC", children = 3, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX1S2BAC", children = 1, buttons = 2, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 4cd241d156..bb50903777 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Matrix X Series 4x2 1",矩阵系列4x2 1 "Matrix X Series 3x2 1",矩阵系列3x2 1 "Matrix X Series 2x2 1",矩阵系列2x2 1 "Matrix X Series 1x2 1",矩阵系列1x2 1 From eacd0517066177b5b01672bef1a7f9f26574da3a Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:07:29 +0800 Subject: [PATCH 14/24] add firstled-io_P1S1BAC (#3067) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index a6b565216f..117b85b72b 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/P1S1BAC" + deviceLabel: Punk Series 1x1 1 + manufacturer: FIRSTLED + model: P1S1BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/MX4S2BAC" deviceLabel: Matrix X Series 4x2 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 1db3711245..4264aeb477 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "P1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX4S2BAC", children = 4, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX3S2BAC", children = 3, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index bb50903777..452bd6119a 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Punk Series 1x1 1",朋克系列1x1 1 "Matrix X Series 4x2 1",矩阵系列4x2 1 "Matrix X Series 3x2 1",矩阵系列3x2 1 "Matrix X Series 2x2 1",矩阵系列2x2 1 From fb83efe355fc2ec9ac7adee2ac351f10d2db98c2 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:13:00 +0800 Subject: [PATCH 15/24] add firstled-io_P2S2BAC (#3068) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 117b85b72b..f5c6b29006 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/P2S2BAC" + deviceLabel: Punk Series 2x2 1 + manufacturer: FIRSTLED + model: P2S2BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/P1S1BAC" deviceLabel: Punk Series 1x1 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 4264aeb477..3e7ecf47f2 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "P2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "P1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX4S2BAC", children = 4, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX3S2BAC", children = 3, buttons = 2, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 452bd6119a..5906810402 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Punk Series 2x2 1",朋克系列2x2 1 "Punk Series 1x1 1",朋克系列1x1 1 "Matrix X Series 4x2 1",矩阵系列4x2 1 "Matrix X Series 3x2 1",矩阵系列3x2 1 From 0adce659ef3f20880948df0e4583fd016516c70f Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:18:33 +0800 Subject: [PATCH 16/24] add firstled-io_P3S3BAC (#3069) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index f5c6b29006..fbd5adaaf0 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/P3S3BAC" + deviceLabel: Punk Series 3x3 1 + manufacturer: FIRSTLED + model: P3S3BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/P2S2BAC" deviceLabel: Punk Series 2x2 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 3e7ecf47f2..5b50a09368 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "P3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "P2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "P1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "MX4S2BAC", children = 4, buttons = 2, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index 5906810402..f23ee9fe68 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Punk Series 3x3 1",朋克系列3x3 1 "Punk Series 2x2 1",朋克系列2x2 1 "Punk Series 1x1 1",朋克系列1x1 1 "Matrix X Series 4x2 1",矩阵系列4x2 1 From fe336b6e905b3da382911306855b468107d8fd13 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 10 Jul 2026 12:14:56 -0500 Subject: [PATCH 17/24] Matter Switch: Support 3R Garage Door Subdriver (#2702) --- .../matter-switch/fingerprints.yml | 5 + .../profiles/garage-door-battery.yml | 14 + .../SmartThings/matter-switch/src/init.lua | 1 + .../third_reality_garage_door/can_handle.lua | 11 + .../third_reality_garage_door/init.lua | 92 +++ .../third_reality_mk1/can_handle.lua | 7 +- .../sub_drivers/third_reality_mk1/init.lua | 1 + .../matter-switch/src/switch_utils/fields.lua | 10 +- .../src/test/test_third_reality_mk1.lua | 241 -------- .../test/test_third_reality_subdrivers.lua | 533 ++++++++++++++++++ 10 files changed, 666 insertions(+), 249 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/garage-door-battery.yml create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/can_handle.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/init.lua delete mode 100644 drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua create mode 100644 drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 8637b37713..930e38e559 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -269,6 +269,11 @@ matterManufacturer: vendorId: 0x1407 productId: 0x1388 deviceProfileName: 12-button-keyboard + - id: "5127/4248" + deviceLabel: Smart Garage Door Opener + vendorId: 0x1407 + productId: 0x1098 + deviceProfileName: garage-door-battery #Edishine - id: "5458/3328" diff --git a/drivers/SmartThings/matter-switch/profiles/garage-door-battery.yml b/drivers/SmartThings/matter-switch/profiles/garage-door-battery.yml new file mode 100644 index 0000000000..bbab3c16ad --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/garage-door-battery.yml @@ -0,0 +1,14 @@ +name: garage-door-battery +components: +- id: main + capabilities: + - id: doorControl + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: GarageDoor diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index e2a04e103a..577820c171 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -375,6 +375,7 @@ local matter_driver_template = { switch_utils.lazy_load("sub_drivers.camera"), switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), + switch_utils.lazy_load_if_possible("sub_drivers.third_reality_garage_door"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") }, shared_device_thread_enabled = true, diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/can_handle.lua new file mode 100644 index 0000000000..2e1b734406 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local utils = require "switch_utils.utils" + +return function(opts, driver, device) + if utils.get_product_override_field(device, "is_third_reality_garage_door") then + return true, require("sub_drivers.third_reality_garage_door") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/init.lua new file mode 100644 index 0000000000..4666d091bc --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_garage_door/init.lua @@ -0,0 +1,92 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" + +------------------------------------------------------------------------------------- +-- Third Reality Garage Door Opener specifics +-- +-- This device uses the OnOff cluster to control the door: +-- OnOff = true -> door open +-- OnOff = false -> door closed +-- Commands are mapped from doorControl capability to OnOff cluster commands. +------------------------------------------------------------------------------------- + +local function device_init(driver, device) + -- Force a subscription to the OnOff cluster, since doorControl does not explicitly map to it in the default driver. + device:add_subscribed_attribute(clusters.OnOff.attributes.OnOff) + device:add_subscribed_attribute(clusters.PowerSource.attributes.BatPercentRemaining) + device:subscribe() +end + +local function match_profile(driver, device) + device:try_update_metadata({profile = "garage-door-battery"}) +end + +-- Prevent any of the main driver's logic from running +local function device_added(driver, device) end + +-- Prevent any of the main driver's logic from running +local function info_changed(driver, device, event, args) end + +local function do_configure(driver, device) + match_profile(driver, device) +end + +local function driver_switched(driver, device) + match_profile(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) +end + + +local function on_off_attr_handler(driver, device, ib, response) + if ib.data.value then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.doorControl.door.open()) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.doorControl.door.closed()) + end +end + +local function handle_door_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:emit_event_for_endpoint(endpoint_id, capabilities.doorControl.door.opening()) + device:send(clusters.OnOff.server.commands.On(device, endpoint_id)) +end + +local function handle_door_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + device:emit_event_for_endpoint(endpoint_id, capabilities.doorControl.door.closing()) + device:send(clusters.OnOff.server.commands.Off(device, endpoint_id)) +end + +local third_reality_garage_door_handler = { + NAME = "ThirdReality Garage Door Handler", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + driverSwitched = driver_switched, + infoChanged = info_changed, + }, + matter_handlers = { + attr = { + [clusters.OnOff.ID] = { + [clusters.OnOff.attributes.OnOff.ID] = on_off_attr_handler, + }, + }, + }, + capability_handlers = { + [capabilities.doorControl.ID] = { + [capabilities.doorControl.commands.open.NAME] = handle_door_open, + [capabilities.doorControl.commands.close.NAME] = handle_door_close, + }, + }, + supported_capabilities = { + capabilities.doorControl, + capabilities.battery, + }, + can_handle = require("sub_drivers.third_reality_garage_door.can_handle") +} + +return third_reality_garage_door_handler diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua index f3f5342989..f68e568639 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/third_reality_mk1/can_handle.lua @@ -1,13 +1,10 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local device_lib = require "st.device" +local utils = require "switch_utils.utils" return function(opts, driver, device) - local THIRD_REALITY_MK1_FINGERPRINT = { vendor_id = 0x1407, product_id = 0x1388 } - if device.network_type == device_lib.NETWORK_TYPE_MATTER and - device.manufacturer_info.vendor_id == THIRD_REALITY_MK1_FINGERPRINT.vendor_id and - device.manufacturer_info.product_id == THIRD_REALITY_MK1_FINGERPRINT.product_id then + if utils.get_product_override_field(device, "is_third_reality_mk1") then return true, require("sub_drivers.third_reality_mk1") end return 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 9c80c247be..8ba2bd28a1 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 @@ -81,6 +81,7 @@ end local function driver_switched(driver, device) match_profile(driver, device) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) end local function initial_press_event_handler(driver, device, ib, response) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 02c864fa4f..a8e9781172 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -120,9 +120,13 @@ SwitchFields.vendor_overrides = { [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, }, - [0x1209] = { -- Bosch - [0x3013] = {target_profile = "light-level-battery-illuminance-motion-temperature"} - } + [0x1209] = { -- BOSCH_MANUFACTURER_ID + [0x3013] = {target_profile = "light-level-battery-illuminance-motion-temperature"}, + }, + [0x1407] = { -- THIRD_REALITY_MANUFACTURER_ID + [0x1098] = { is_third_reality_garage_door = true }, + [0x1388] = { is_third_reality_mk1 = true}, + }, } SwitchFields.switch_category_vendor_overrides = { 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 deleted file mode 100644 index b7e94797f6..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_mk1.lua +++ /dev/null @@ -1,241 +0,0 @@ --- Copyright © 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local dkjson = require "dkjson" -local t_utils = require "integration_test.utils" -local test = require "integration_test" -local utils = require "st.utils" - -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("12-button-keyboard.yml"), - manufacturer_info = {vendor_id = 0x1407, product_id = 0x1388}, - 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 = 1, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 2, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 3, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 4, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 5, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 6, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 7, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 8, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 9, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 10, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 11, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - }, - { - endpoint_id = 12, - clusters = { - { - cluster_id = clusters.Switch.ID, - feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, - cluster_type = "SERVER" - } - }, - device_types = { - {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch - } - } - } -}) - -local function configure_buttons() - for key = 1, 12 do - 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}}))) - end -end - -local function test_init() - test.disable_startup_messages() - test.mock_device.add_test_device(mock_device) - local cluster_subscribe_list = { - clusters.Switch.events.InitialPress - } - local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) - for i, clus in ipairs(cluster_subscribe_list) do - if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end - end - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) - - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - mock_device:expect_metadata_update({ profile = "12-button-keyboard" }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - configure_buttons() - - local device_info_copy = utils.deep_copy(mock_device.raw_st_data) - device_info_copy.profile.id = "12-buttons-keyboard" - local device_info_json = dkjson.encode(device_info_copy) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) - configure_buttons() - test.socket.matter:__expect_send({mock_device.id, subscribe_request}) -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "Handle single press sequence", - function() - for key = 1, 12 do - test.socket.matter:__queue_receive({ - mock_device.id, - clusters.Switch.events.InitialPress:build_test_event_report(mock_device, key, {new_position = 1}) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message(key == 1 and "main" or "F" .. key, capabilities.button.button.pushed({state_change = true})) - ) - end - end, - { - min_api_version = 17 - } -) - --- run the tests -test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua b/drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua new file mode 100644 index 0000000000..a32ce8f8ff --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua @@ -0,0 +1,533 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local dkjson = require "dkjson" +local utils = require "st.utils" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("garage-door-battery.yml"), + manufacturer_info = {vendor_id = 0x1407, product_id = 0x1098}, + 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 = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = clusters.PowerSource.types.Feature.BATTERY, + }, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 1} -- On/Off Plug-in Unit + } + } + } +}) + +-- The subscribe list matches the profile capabilities: +-- doorControl -> OnOff.attributes.OnOff +-- battery -> PowerSource.attributes.BatPercentRemaining +-- The subdriver overrides device_init, so the main driver's extend_device("subscribe", ...) +-- is not called; the default device:subscribe() is used, which does not add AttributeList. +local cluster_subscribe_list = { + clusters.OnOff.attributes.OnOff, + clusters.PowerSource.attributes.BatPercentRemaining, +} + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, clus in ipairs(cluster_subscribe_list) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + + -- added lifecycle: subdriver overrides device_added to a no-op so no subscribe here + test.socket.device_lifecycle:__queue_receive({mock_device.id, "added"}) + + -- init lifecycle: device_init subscribes + test.socket.device_lifecycle:__queue_receive({mock_device.id, "init"}) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + -- doConfigure: sets battery support field and updates profile metadata + test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({profile = "garage-door-battery"}) + mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) +end + +test.set_test_init_function(test_init) + +-- ── Attribute handler tests ────────────────────────────────────────────────── + +test.register_message_test( + "OnOff true should emit door.open", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 1, true) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.doorControl.door.open()) + } + } +) + +test.register_message_test( + "OnOff false should emit door.closed", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(mock_device, 1, false) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.doorControl.door.closed()) + } + } +) + +-- ── Capability command tests ───────────────────────────────────────────────── + +test.register_message_test( + "doorControl open command should emit opening then send OnOff.On", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + {capability = "doorControl", component = "main", command = "open", args = {}} + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.doorControl.door.opening()) + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.On(mock_device, 1) + } + } + } +) + +test.register_message_test( + "doorControl close command should emit closing then send OnOff.Off", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + {capability = "doorControl", component = "main", command = "close", args = {}} + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.doorControl.door.closing()) + }, + { + channel = "matter", + direction = "send", + message = { + mock_device.id, + clusters.OnOff.server.commands.Off(mock_device, 1) + } + } + } +) + +-- ── Battery attribute tests ─────────────────────────────────────────────────── + +test.register_message_test( + "BatPercentRemaining report should emit correct battery percentage", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data(mock_device, 1, 200) + } + }, + { + channel = "capability", + direction = "send", + -- BatPercentRemaining is in units of 0.5%, so 200 = 100% + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +test.register_message_test( + "BatPercentRemaining report of 150 should emit 75% battery", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data(mock_device, 1, 150) + } + }, + { + channel = "capability", + direction = "send", + -- 150 * 0.5 = 75% + message = mock_device:generate_test_message("main", capabilities.battery.battery(75)) + } + } +) + +-- ── Profile / driverSwitched tests ──────────────────────────────────────────── + +test.register_coroutine_test( + "doConfigure should set garage-door-battery profile", + function() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "doConfigure"}) + mock_device:expect_metadata_update({profile = "garage-door-battery"}) + mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) + end, + {min_api_version = 17} +) + +test.register_coroutine_test( + "driverSwitched should restore garage-door-battery profile", + function() + test.socket.device_lifecycle:__queue_receive({mock_device.id, "driverSwitched"}) + mock_device:expect_metadata_update({profile = "garage-door-battery"}) + mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) + end, + {min_api_version = 17} +) + + +local mock_device_misprofiled = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("light-binary.yml"), + manufacturer_info = {vendor_id = 0x1407, product_id = 0x1098}, + 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 = 1, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0, + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = clusters.PowerSource.types.Feature.BATTERY, + }, + }, + device_types = { + {device_type_id = 0x010A, device_type_revision = 1} -- On/Off Plug-in Unit + } + } + } +}) + +test.register_coroutine_test( + "doConfigure should correct profile if misprofiled", + function() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device_misprofiled) + + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_misprofiled) + for i, clus in ipairs(cluster_subscribe_list) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device_misprofiled)) end + end + + -- added lifecycle: subdriver overrides device_added to a no-op so no subscribe here + test.socket.device_lifecycle:__queue_receive({mock_device_misprofiled.id, "added"}) + + -- init lifecycle: device_init subscribes + test.socket.device_lifecycle:__queue_receive({mock_device_misprofiled.id, "init"}) + test.socket.matter:__expect_send({mock_device_misprofiled.id, subscribe_request}) + + -- doConfigure: sets battery support field and updates profile metadata + test.socket.device_lifecycle:__queue_receive({mock_device_misprofiled.id, "doConfigure"}) + mock_device_misprofiled:expect_metadata_update({profile = "garage-door-battery"}) + mock_device_misprofiled:expect_metadata_update({provisioning_state = "PROVISIONED"}) + end, + { + test_init = function() test.mock_device.add_test_device(mock_device_misprofiled) end, + } +) + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("12-button-keyboard.yml"), + manufacturer_info = {vendor_id = 0x1407, product_id = 0x1388}, + 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 = 1, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 2, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 4, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 5, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 6, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 7, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 8, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 9, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 11, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + }, + { + endpoint_id = 12, + clusters = { + { + cluster_id = clusters.Switch.ID, + feature_map = clusters.Switch.types.Feature.MOMENTARY_SWITCH, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x000F, device_type_revision = 1} -- Generic Switch + } + } + } +}) + +local function configure_buttons() + for key = 1, 12 do + 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}}))) + end +end + +local function test_init_mk1() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + local cluster_subscribe_list = { + clusters.Switch.events.InitialPress + } + local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device) + for i, clus in ipairs(cluster_subscribe_list) do + if i > 1 then subscribe_request:merge(clus:subscribe(mock_device)) end + end + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + mock_device:expect_metadata_update({ profile = "12-button-keyboard" }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + configure_buttons() + + local device_info_copy = utils.deep_copy(mock_device.raw_st_data) + device_info_copy.profile.id = "12-buttons-keyboard" + local device_info_json = dkjson.encode(device_info_copy) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "infoChanged", device_info_json }) + configure_buttons() + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) +end + +test.register_coroutine_test( + "Handle single press sequence", + function() + for key = 1, 12 do + test.socket.matter:__queue_receive({ + mock_device.id, + clusters.Switch.events.InitialPress:build_test_event_report(mock_device, key, {new_position = 1}) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message(key == 1 and "main" or "F" .. key, capabilities.button.button.pushed({state_change = true})) + ) + end + end, + { + test_init = test_init_mk1, + min_api_version = 17 + } +) + +-- run the tests +test.run_registered_tests() From 074ee39b87bd4f5df00af3aa660df6d5bdf47241 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 1 Jul 2026 09:27:59 -0500 Subject: [PATCH 18/24] Merge pull request #2858 from JanJakubiszyn/Hager_WAASYS_device_support Adding support for Hager WAASYS devices --- .../matter-switch/fingerprints.yml | 20 + .../profiles/motion-illuminance.yml | 14 + .../profiles/window-covering.yml | 21 + .../SmartThings/matter-switch/src/init.lua | 3 +- .../src/sub_drivers/hager/can_handle.lua | 13 + .../src/sub_drivers/hager/init.lua | 475 +++++ .../switch_handlers/attribute_handlers.lua | 2 +- .../src/switch_utils/device_configuration.lua | 1 - .../matter-switch/src/switch_utils/fields.lua | 10 + .../src/test/test_hager_waasys.lua | 1776 +++++++++++++++++ 10 files changed, 2332 insertions(+), 3 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/window-covering.yml create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 930e38e559..d6a367c685 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -909,6 +909,26 @@ matterManufacturer: vendorId: 0x1285 productId: 0x0009 deviceProfileName: 4-button-batteryLevel + - id: "4741/5" + deviceLabel: Hager Switch 1G + vendorId: 0x1285 + productId: 0x0005 + deviceProfileName: matter-bridge + - id: "4741/6" + deviceLabel: Hager Switch 2G + vendorId: 0x1285 + productId: 0x0006 + deviceProfileName: matter-bridge + - id: "4741/7" + deviceLabel: Hager PIR 1.1M + vendorId: 0x1285 + productId: 0x0007 + deviceProfileName: matter-bridge + - id: "4741/10" + deviceLabel: Hager PIR 2.2M + vendorId: 0x1285 + productId: 0x000A + deviceProfileName: matter-bridge # HAOJAI - id: "5530/4113" deviceLabel: HAOJAI Smart Switch 3-key diff --git a/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml b/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml new file mode 100644 index 0000000000..dfe052043c --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml @@ -0,0 +1,14 @@ +name: motion-illuminance +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor diff --git a/drivers/SmartThings/matter-switch/profiles/window-covering.yml b/drivers/SmartThings/matter-switch/profiles/window-covering.yml new file mode 100644 index 0000000000..b588b41ec9 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/window-covering.yml @@ -0,0 +1,21 @@ +name: window-covering +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadePreset + version: 1 + - id: windowShadeLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +preferences: + - preferenceId: presetPosition + explicit: true + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 577820c171..31229b7838 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -1,4 +1,4 @@ --- Copyright © 2025 SmartThings, Inc. +-- Copyright © 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local MatterDriver = require "st.matter.driver" @@ -374,6 +374,7 @@ local matter_driver_template = { switch_utils.lazy_load_if_possible("sub_drivers.aqara_cube"), switch_utils.lazy_load("sub_drivers.camera"), switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), + switch_utils.lazy_load_if_possible("sub_drivers.hager"), switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_garage_door"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua new file mode 100644 index 0000000000..3ac75a5805 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local device_lib = require "st.device" + local get_product_override_field = require "switch_utils.utils".get_product_override_field + + local checked_device = device.network_type == device_lib.NETWORK_TYPE_MATTER and device or device:get_parent_device() + if get_product_override_field(checked_device, "needs_hager_subdriver") then + return true, require("sub_drivers.hager") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua new file mode 100644 index 0000000000..448295247d --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua @@ -0,0 +1,475 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local device_lib = require "st.device" +local buttonCfg = require "switch_utils.device_configuration".ButtonCfg +local create_child = require "switch_utils.device_configuration".ChildCfg.create_or_update_child_devices +local switch_utils = require "switch_utils.utils" +local fields = require "switch_utils.fields" + +local ACTIVE_EPS = "__active_EPS" +local MATTER_DEVICE_ID = "MATTER_DEVICE_ID" +local PARENT_ID = "PARENT_ID" +local CURRENT_LIFT = "__current_lift" +local BUTTON_EPS = "__button_eps" +local MAIN_WC_EP = "__main_wc_ep" + +local PRODUCT_ID = { + SWITCH_1G = 0x0005, + SWITCH_2G = 0x0006, + PIR_1_1M = 0x0007, + PIR_2_2M = 0x000A, +} + +local function subscribe (device, endpoint_id, cluster_id, attr_id, event_id) + device:send(cluster_base.subscribe(device, endpoint_id, cluster_id, attr_id, event_id)) +end + +local function get_parent (driver, device) + return driver:get_device_info(device:get_field(PARENT_ID) or nil) +end + +local function get_matter_device(device) + local matter_device_id = device:get_field(MATTER_DEVICE_ID) + if matter_device_id then + local driver = device.driver + if driver then + local matter_device = driver:get_device_info(matter_device_id) + if matter_device then + return matter_device + end + end + end + return nil +end + +local function extract_reported_endpoints(ib) + local eps = {} + if ib.data and ib.data.elements then + for _, el in ipairs(ib.data.elements) do + local ep_id = el.value + if type(ep_id) == "number" and ep_id ~= 1 and ep_id ~= 2 then + table.insert(eps, ep_id) + end + end + end + return eps +end + +local function detect_endpoint_changes (device, ib_elements) + local stored_eps = device:get_field(ACTIVE_EPS) or {} + ib_elements = ib_elements or {} + + local old_set, new_set = {}, {} + for _, ep_id in ipairs(stored_eps) do + old_set[ep_id] = true + end + for _, ep_id in ipairs(ib_elements) do + new_set[ep_id] = true + end + + local removed_eps, added_eps = {}, {} + + for ep_id in pairs(old_set) do + if not new_set[ep_id] then + table.insert(removed_eps, ep_id) + end + end + for ep_id in pairs(new_set) do + if not old_set[ep_id] then + table.insert(added_eps, ep_id) + end + end + return removed_eps, added_eps +end + +local function assign_profile_for_endpoint(device_type_id) + local profile = fields.device_type_profile_map[device_type_id] or "switch-binary" + return function(device, ep_id, is_child_device) + return profile, nil + end +end + +local function link_matter_device_and_parent(matter_device) + local parent = matter_device:get_parent_device() + matter_device:set_field(PARENT_ID, parent.id, { persist = true }) + matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(PARENT_ID, parent.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) +end + +local function handle_set_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local lift_value = device.preferences.presetPosition + local hundredths_lift_percent = (100 - tonumber(lift_value)) * 100 + device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage(device, endpoint_id, hundredths_lift_percent)) +end + +local function handle_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) + if device.preferences.reverse then + req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) + end + device:send(req) +end + +local function handle_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) + if device.preferences.reverse then + req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) + end + device:send(req) +end + +local function handle_pause(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.WindowCovering.server.commands.StopMotion(device, endpoint_id) + device:send(req) +end + +local function handle_shade_level(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local lift_percentage_value = 100 - cmd.args.shadeLevel + local hundredths_lift_percentage = lift_percentage_value * 100 + local req = clusters.WindowCovering.server.commands.GoToLiftPercentage( + device, endpoint_id, hundredths_lift_percentage + ) + device:send(req) +end + +local current_pos_handler = function(attribute) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local windowShade = capabilities.windowShade.windowShade + local position = 100 - math.floor(ib.data.value / 100) + local reverse = device.preferences.reverse + device:emit_event_for_endpoint(ib.endpoint_id, attribute(position)) + if attribute == capabilities.windowShadeLevel.shadeLevel then + device:set_field(CURRENT_LIFT, position) + end + local lift_position = device:get_field(CURRENT_LIFT) + if lift_position == 100 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closed() or windowShade.open()) + elseif lift_position > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) + elseif lift_position == 0 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) + end + end +end + +local function current_status_handler(driver, device, ib, response) + local windowShade = capabilities.windowShade.windowShade + local reverse = device.preferences.reverse + local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL + if state == 1 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closing() or windowShade.opening()) + elseif state == 2 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.opening() or windowShade.closing()) + elseif state ~= 0 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) + end +end + +local function info_changed(driver, device, event, args) + if not device or not device.id or not device.profile then + return + end + + if device.profile.id ~= args.old_st_store.profile.id or device.network_type == device_lib.NETWORK_TYPE_CHILD then + local parent = device:get_parent_device() + local matter_device = get_matter_device(parent) + local map = {} + device.thread:call_with_delay(2, function() + if device:supports_capability(capabilities.button) then + local button_eps = parent:get_field(BUTTON_EPS) + local clean_eps = {} + table.sort(button_eps) + + for _, v in ipairs(button_eps or {}) do + table.insert(clean_eps, v) + end + + buttonCfg.update_button_component_map(matter_device, clean_eps[1], clean_eps) + for _, ep_id in ipairs(clean_eps) do + subscribe(device, ep_id, clusters.Switch.ID, nil, clusters.Switch.events.MultiPressComplete.ID) + subscribe(device, ep_id, clusters.Switch.ID, nil, clusters.Switch.events.LongPress.ID) + device:emit_event_for_endpoint(ep_id, capabilities.button.supportedButtonValues({ "pushed", "double", "held" })) + end + return + elseif device:supports_capability(capabilities.switch) then + map = { main = 3 } + subscribe(device, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) + if device:supports_capability(capabilities.switchLevel) then + map = { main = 4 } + subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) + subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID) + subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID) + end + elseif device:supports_capability(capabilities.motionSensor) then + subscribe(device, nil, clusters.OccupancySensing.ID, clusters.OccupancySensing.attributes.Occupancy.ID, nil) + subscribe(device, nil, clusters.IlluminanceMeasurement.ID, clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID, nil) + elseif device:supports_capability(capabilities.windowShadeLevel) then + map = { main = 5 } + subscribe(device, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID) + subscribe(device, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) + end + if device.id == matter_device.id then + parent:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, map, { persist = true }) + matter_device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, map, { persist = true }) + end + end) + end +end + +local function device_init (driver, device) + if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then + device.thread:call_with_delay(4, function() + info_changed(driver, device, nil, { + old_st_store = { profile = { } } + }) + end) + return + end + + if device:get_parent_device() ~= nil then + link_matter_device_and_parent(device) + local parent = device:get_parent_device() + device:extend_device("send", function(self, message) + return parent:send(message) + end) + else + subscribe(device, 2, clusters.Descriptor.ID, clusters.Descriptor.attributes.PartsList.ID) + device.thread:call_with_delay(3, function() + local matter_device = get_matter_device(device) + device:extend_device("emit_event_for_endpoint", function(self, ep_info, event) + local endpoint_id = type(ep_info) == "number" and ep_info or ep_info.endpoint_id + local child = self:get_child_by_parent_assigned_key(string.format("%d", endpoint_id)) + if child then + return child:emit_event(event) + end + if matter_device then + return matter_device:emit_event_for_endpoint(ep_info, event) + end + end) + end) + end + + device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) +end + +local function handle_descriptor_report(driver, device, ib, response) + local product_id = device.manufacturer_info and device.manufacturer_info.product_id + local parent = get_parent(driver, device) + + if not parent or ib.endpoint_id ~= 2 then + return + end + + local matter_device = get_matter_device(device) + + local new_eps = extract_reported_endpoints(ib) or {} + table.sort(new_eps) + local removed_eps, added_eps = detect_endpoint_changes(device, new_eps) + + device:set_field(ACTIVE_EPS, new_eps, { persist = true }) + + for _, ep_id in ipairs(added_eps or {}) do + subscribe(parent, ep_id, clusters.Descriptor.ID, clusters.Descriptor.attributes.DeviceTypeList.ID, nil) + + if product_id == PRODUCT_ID.SWITCH_1G and ep_id == 3 then + matter_device:try_update_metadata({ profile = "light-binary" }) + elseif product_id == PRODUCT_ID.SWITCH_2G then + if ep_id == 3 then + local profile = switch_utils.tbl_contains(new_eps, 4) and "light-binary" or "2-button" + matter_device:try_update_metadata({ profile = profile }) + elseif ep_id == 4 then + local profile = switch_utils.tbl_contains(new_eps, 3) and "light-binary" or "2-button" + matter_device:try_update_metadata({ profile = profile }) + end + end + end + + for _, ep_id in ipairs(removed_eps) do + local button_eps = parent:get_field(BUTTON_EPS) or {} + local clean_eps = {} + + for _, value in ipairs(button_eps) do + if value ~= ep_id then + table.insert(clean_eps, value) + end + end + table.sort(clean_eps) + parent:set_field(BUTTON_EPS, clean_eps, { persist = true }) + matter_device:set_field(BUTTON_EPS, clean_eps, { persist = true }) + + local child = parent:get_child_by_parent_assigned_key(tostring(ep_id)) + if child then + driver:try_delete_device(child.id) + end + + if ep_id == 3 then + if device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then + matter_device:try_update_metadata({ profile = "2-button" }) + elseif device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then + local has_ep4 = switch_utils.tbl_contains(new_eps, 4) + matter_device:try_update_metadata({ profile = has_ep4 and "2-button" or "4-button" }) + end + elseif ep_id == 4 then + if device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then + local button_comb = switch_utils.tbl_contains(new_eps, 3) + if button_comb then + matter_device:try_update_metadata({ profile = "2-button" }) + create_child(driver, parent, { 3 }, 1, assign_profile_for_endpoint(fields.DEVICE_TYPE_ID.LIGHT.ON_OFF)) + else + matter_device:try_update_metadata({ profile = "4-button" }) + end + end + end + end +end + +local function device_type_handler (driver, device, ib) + local parent = get_parent(driver, device) + if not parent then + return + end + + local matter_device = get_matter_device(device) + local stored_btn_eps = parent:get_field(BUTTON_EPS) or {} + local new_btn_eps = {} + local ep_id = ib.endpoint_id + local value = ib.data.elements + + for _, v in ipairs(stored_btn_eps) do + table.insert(new_btn_eps, v) + end + for _, element in ipairs(value) do + local device_type_field = element.elements.device_type + local device_type_id = device_type_field and device_type_field.value + + if device_type_id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH then + if not switch_utils.tbl_contains(new_btn_eps, ep_id) then + switch_utils.set_field_for_endpoint(parent, fields.SUPPORTS_MULTI_PRESS, ep_id) + switch_utils.set_field_for_endpoint(parent, fields.IGNORE_NEXT_MPC, ep_id) + table.insert(new_btn_eps, ep_id) + table.sort(new_btn_eps) + parent:set_field(BUTTON_EPS, new_btn_eps, { persist = true }) + end + end + + if device_type_id == fields.DEVICE_TYPE_ID.LIGHT.ON_OFF then + local active_eps = device:get_field(ACTIVE_EPS) + + if ep_id == 3 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then + return + elseif ep_id == 4 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then + if switch_utils.tbl_contains(active_eps, 3) then + local ep3 = parent:get_child_by_parent_assigned_key("3") + if ep3 then + driver:try_delete_device(ep3.id) + end + else + create_child(driver, parent, { 4 }, 1, assign_profile_for_endpoint(device_type_id)) + return + end + elseif ep_id == 3 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then + if not switch_utils.tbl_contains(active_eps, 4) then + create_child(driver, parent, { 3 }, 1, assign_profile_for_endpoint(device_type_id)) + else + driver:try_delete_device(ep_id) + end + return + end + create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) + elseif device_type_id == fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE then + if ep_id == 4 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then + return + end + create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) + elseif device_type_id == fields.DEVICE_TYPE_ID.WINDOW_COVERING then + if matter_device:get_field(MAIN_WC_EP) == nil then + create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) + end + end + end +end + +local function do_configure (driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + + local wc_eps = device:get_endpoints(clusters.WindowCovering.ID) + local oc_eps = device:get_endpoints(clusters.OccupancySensing.ID) + local bt_eps = device:get_endpoints(clusters.Switch.ID) + local lvl_eps = device:get_endpoints(clusters.LevelControl.ID) + local product_id = device.manufacturer_info.product_id + + table.sort(wc_eps) + table.sort(oc_eps) + table.sort(lvl_eps) + + if #oc_eps > 0 then + device:try_update_metadata({ profile = "motion-illuminance" }) + elseif #wc_eps > 0 and product_id == PRODUCT_ID.SWITCH_1G then + device:try_update_metadata({ profile = "window-covering" }) + device:set_field(MAIN_WC_EP, wc_eps[1], { persist = true }) + elseif #bt_eps == 4 then + device:try_update_metadata({ profile = "4-button" }) + elseif #bt_eps == 2 then + device:try_update_metadata({ profile = "2-button" }) + elseif #lvl_eps > 0 and product_id == PRODUCT_ID.SWITCH_1G then + device:try_update_metadata({ profile = "light-level" }) + end + device:set_field(BUTTON_EPS, bt_eps, { persist = true }) + end +end + +local function added (driver, device) end +local function driver_switched(driver, device) end + +local hager_switch = { + NAME = "Hager Subdriver", + lifecycle_handlers = { + added = added, + init = device_init, + infoChanged = info_changed, + doConfigure = do_configure, + driverSwitched = driver_switched + }, + matter_handlers = { + attr = { + [clusters.Descriptor.ID] = { + [clusters.Descriptor.attributes.PartsList.ID] = handle_descriptor_report, + [clusters.Descriptor.attributes.DeviceTypeList.ID] = device_type_handler, + }, + [clusters.WindowCovering.ID] = { + [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_pos_handler(capabilities.windowShadeLevel.shadeLevel), + [clusters.WindowCovering.attributes.OperationalStatus.ID] = current_status_handler, + }, + }, + }, + capability_handlers = { + [capabilities.windowShadePreset.ID] = { + [capabilities.windowShadePreset.commands.presetPosition.NAME] = handle_set_preset, + }, + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.close.NAME] = handle_close, + [capabilities.windowShade.commands.open.NAME] = handle_open, + [capabilities.windowShade.commands.pause.NAME] = handle_pause, + }, + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = handle_shade_level, + }, + }, + can_handle = require("sub_drivers.hager.can_handle") +} + +return hager_switch diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index 45694bc613..b7feed6344 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -246,7 +246,7 @@ end -- [[ OCCUPANCY CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.occupancy_handler(driver, device, ib, response) - device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) + device:emit_event_for_endpoint(ib.endpoint_id, ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) 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 772669ebd7..9c5235d1e2 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -156,7 +156,6 @@ function ButtonDeviceConfiguration.update_button_component_map(device, default_e device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) end - function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) local msr_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE}) local msl_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index a8e9781172..5873cb40b2 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -34,7 +34,9 @@ SwitchFields.DEVICE_TYPE_ID = { ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, GENERIC_SWITCH = 0x000F, + LIGHT_SENSOR = 0x0106, IRRIGATION_SYSTEM = 0x0040, + OCCUPANCY_SENSOR = 0x0107, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, ON_OFF_PLUG_IN_UNIT = 0x010A, @@ -50,6 +52,7 @@ SwitchFields.DEVICE_TYPE_ID = { COLOR_DIMMER = 0x0105, }, WATER_VALVE = 0x0042, + WINDOW_COVERING = 0x0202 } SwitchFields.device_type_profile_map = { @@ -64,6 +67,7 @@ SwitchFields.device_type_profile_map = { [SwitchFields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT] = "plug-level", [SwitchFields.DEVICE_TYPE_ID.MOUNTED_ON_OFF_CONTROL] = "switch-binary", [SwitchFields.DEVICE_TYPE_ID.MOUNTED_DIMMABLE_LOAD_CONTROL] = "switch-level", + [SwitchFields.DEVICE_TYPE_ID.WINDOW_COVERING] = "window-covering" } -- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for @@ -116,6 +120,12 @@ SwitchFields.vendor_overrides = { [0x1189] = { -- LEDVANCE_MANUFACTURER_ID [0x0891] = { target_profile = "switch-binary", initial_profile = "light-binary" }, }, + [0x1285] = { -- HAGER_MANUFACTURER_ID + [0x0005] = { needs_hager_subdriver = true }, -- Hager HBnet 1g switch + [0x0006] = { needs_hager_subdriver = true }, -- Hager HBnet 2g switch + [0x0007] = { needs_hager_subdriver = true }, -- Hager HBnet PIR 1.1M + [0x000A] = { needs_hager_subdriver = true }, -- Hager HBnet PIR 2.2M + }, [0x1321] = { -- SONOFF_MANUFACTURER_ID [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, diff --git a/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua new file mode 100644 index 0000000000..2a1652a2be --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua @@ -0,0 +1,1776 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local data_types = require "st.matter.data_types" +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local descriptor = require "st.matter.generated.zap_clusters.Descriptor" + +local MATTER_DEVICE_ID = "MATTER_DEVICE_ID" +local PARENT_ID = "PARENT_ID" +local BUTTON_EPS = "__button_eps" + +local function create_parent_device(product_id) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 4x Button", + profile = t_utils.get_profile_definition("matter-bridge.yml"), + manufacturer_info = { + vendor_id = 0x1285, + product_id = product_id, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 1, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } + }, + device_types = { { device_type_id = 0x000E, device_type_revision = 1 } } -- AggregateNode + }, + { + endpoint_id = 2, + clusters = { + { + cluster_id = clusters.Descriptor.ID, + cluster_type = "SERVER", + cluster_revision = 1, + } + }, + device_types = { { device_type_id = 0x0039, device_type_revision = 1 } } -- BridgedNode + } + } + }) +end + +local function create_matter_device(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 4x Button", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0006, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 8, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 9, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 11, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + } + }) +end + +-- Create Hager 2G Relay device with endpoints 3 & 4 (OnOff clusters) +local function create_hager_2g_relay(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 2G Relay", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0006, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + } + }, + device_types = { { device_type_id = 0x0100, device_type_revision = 1 } } + }, + { + endpoint_id = 4, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + } + }, + device_types = { { device_type_id = 0x0100, device_type_revision = 1 } } + }, + } + }) +end + +-- Create Hager Dimmer device with ONLY dimmable endpoint (3) - no button endpoints +local function create_hager_dimmer_device_1g(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 Dimmer", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0005, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 4, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + }, + { + cluster_id = clusters.LevelControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 + } + } + }, + device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } + }, + } + }) +end + +local function create_hager_dimmer_device_2g(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 Dimmer", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0006, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + }, + { + cluster_id = clusters.LevelControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 + } + } + }, + device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } + }, + { + endpoint_id = 8, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 9, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + } + }) +end + +local function create_matter_device_with_window(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 matter device with Window Covering", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0006, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 8, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 9, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 12, + clusters = { + { + cluster_id = clusters.WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.WindowCovering.types.Feature.LIFT | + clusters.WindowCovering.types.Feature.POSITION_AWARE_LIFT | + clusters.WindowCovering.types.Feature.ABSOLUTE_POSITION, + attributes = { + [clusters.WindowCovering.attributes.OperationalStatus.ID] = 0x00, + [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = 0x0000, + } + } + }, + device_types = { { device_type_id = 0x0202, device_type_revision = 1 } } + }, + } + }) +end + +local function create_hager_pir_device(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 PIR with Buttons and Motion/Illuminance/Dimmer", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0007, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + }, + { + cluster_id = clusters.LevelControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 + } + } + }, + device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } + }, + { + endpoint_id = 6, + clusters = { + { + cluster_id = clusters.OccupancySensing.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OccupancySensing.attributes.Occupancy.ID] = 0 + } + } + }, + device_types = { { device_type_id = 0x0107, device_type_revision = 1 } } + }, + { + endpoint_id = 5, + clusters = { + { + cluster_id = clusters.IlluminanceMeasurement.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = 0 + } + } + }, + device_types = { { device_type_id = 0x0106, device_type_revision = 1 } } + }, + } + }) +end + +local function add_parent_device(parent) + test.mock_device.add_test_device(parent) + test.socket.device_lifecycle:__queue_receive({ parent.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ parent.id, "init" }) + test.mock_time.advance_time(5) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, 2, descriptor.ID, descriptor.attributes.PartsList.ID, nil) + }) +end + +local function subscribe_switch_events(parent, button_eps) + for _, ep in ipairs(button_eps) do + test.socket.matter:__expect_send({ + parent.id, + clusters.Switch.events.MultiPressComplete:subscribe(parent, ep) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Switch.events.LongPress:subscribe(parent, ep) + }) + end +end + +local function add_matter_device(matter_device, parent, expected_profile_change) + test.mock_device.add_test_device(matter_device) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "doConfigure" }) + matter_device:expect_metadata_update({ profile = expected_profile_change }) + matter_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "init" }) + + matter_device:set_field(PARENT_ID, parent.id, { persist = true }) + matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(PARENT_ID, parent.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) +end + +local function add_switch_matter_device(matter_device, parent) + test.mock_device.add_test_device(matter_device) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "doConfigure" }) + matter_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "init" }) + + matter_device:set_field(PARENT_ID, parent.id, { persist = true }) + matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(PARENT_ID, parent.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) +end + +local function button_supported_values (matter_device) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) +end + +local function initiate_info_changed(device, profile) + test.socket.device_lifecycle:__queue_receive(device:generate_info_changed({ profile = { id = profile } })) + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.mock_time.advance_time(3) +end + +local function configure_parent(device) + test.socket.device_lifecycle:__queue_receive({ device.id, "doConfigure" }) + device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ device.id, "init" }) + test.socket.matter:__expect_send({ + device.id, + cluster_base.subscribe(device, 2, descriptor.ID, descriptor.attributes.PartsList.ID, nil) + }) + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.mock_time.advance_time(3) +end + +local parent = create_parent_device(0x0006) -- 2G button product +local parent_1g = create_parent_device(0x0005) -- 1G button product +local parent_pir = create_parent_device(0x0007) -- PIR product + +local function test_init() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test("Test: 4-Button Device Detection - Profile Changes from matter-bridge to 4-button When Four Button Endpoints Present", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local matter_device = create_matter_device("matter-bridge", parent) + add_matter_device(matter_device, parent, "4-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(10), + data_types.Uint16(11), + })) + }) + + local matter_device_parent_id = matter_device:get_field(PARENT_ID) + local matter_device_id = matter_device:get_field(MATTER_DEVICE_ID) + local parent_id = parent:get_field(PARENT_ID) + local parent_matter_device_id = parent:get_field(MATTER_DEVICE_ID) + + assert(matter_device_parent_id == parent.id, "link_host_and_subhub 1/4") + assert(matter_device_id == matter_device.id, "link_host_and_subhub 2/4") + assert(parent_id == parent.id, "link_host_and_subhub 3/4") + assert(parent_matter_device_id == matter_device.id, "link_host_and_subhub 4/4") + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x08) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x09) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0A) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0B) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 10, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 11, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + +end) + +test.register_coroutine_test("Test: Button Event Handling - Pushed, Double Press, and Held Events on 4-Button Device", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local matter_device = create_matter_device("4-button", parent) + add_matter_device(matter_device, parent, "4-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(10), + data_types.Uint16(11), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x08) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x09) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0A) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0B) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 10, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 11, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.wait_for_events() + initiate_info_changed(matter_device, "4-button") + + subscribe_switch_events(parent, { 8, 9, 10, 11 }) + button_supported_values(matter_device) + + + -- Test single press (pushed) on endpoint 8 (button1) + test.wait_for_events() + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 8, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 1 -- Single press + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true }))) + + --Test double press on endpoint 8 (button1) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 8, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 2 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.double({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + matter_device, 8, { + new_position = 1, + previous_position = 0, + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.held({ state_change = true }))) + + -- Test press on endpoint 9 (button2) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 9, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 1 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.pushed({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 9, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 2 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.double({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + matter_device, 9, { + new_position = 1, + previous_position = 0, + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.held({ state_change = true }))) + + -- Test press on endpoint 10 (button3) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 10, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 1 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.pushed({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 10, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 2 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.double({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + matter_device, 10, { + new_position = 1, + previous_position = 0, + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.held({ state_change = true }))) + + -- Test press on endpoint 11 (button4) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 11, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 1 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.pushed({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 11, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 2 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.double({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + matter_device, 11, { + new_position = 1, + previous_position = 0, + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.held({ state_change = true }))) +end) + +test.register_coroutine_test("Test: Device Type Handler - Handles Button (Type 15) and OnOff (Type 256) Device Types with Child Creation", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local matter_device = create_matter_device("4-button", parent) + add_matter_device(matter_device, parent, "4-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(9), + data_types.Uint16(8), + data_types.Uint16(6), + })) + }) + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 6) + }) + + test.wait_for_events() + + -- Receive DeviceTypeList report with device type 15 (button) for endpoint 8 + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + + -- Receive DeviceTypeList report with device type 256 (OnOff) for endpoint 6 + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 6, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "6" + }) + test.wait_for_events() + initiate_info_changed(matter_device, "2-button") + subscribe_switch_events(parent, { 8, 9 }) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + + test.wait_for_events() + + local child_switch = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("light-binary.yml"), + device_network_id = string.format("%s:6", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "6" + }) + test.wait_for_events() + initiate_info_changed(child_switch, "light-binary") + + test.socket.matter:__expect_send({ + parent.id, + clusters.OnOff.attributes.OnOff:subscribe(parent, nil) + }) +end) + +test.register_coroutine_test("Test: 2G Relay - Profile Changes Between light-binary and 2-button Based On Endpoint Availability", function() + test.socket.matter:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + add_parent_device(parent) + local relay = create_hager_2g_relay("light-binary", parent) + add_switch_matter_device(relay, parent) + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + -- Scenario 1: EP3 + EP4 present → light-binary profile + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(3), + data_types.Uint16(4), + })) + }) + -- + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 3) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) + }) + + relay:expect_metadata_update({ profile = "light-binary" }) + relay:expect_metadata_update({ profile = "light-binary" }) + test.wait_for_events() + -- Both endpoints are device type 256 (OnOff) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 3, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "4" + }) + local child_switch = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("light-binary.yml"), + device_network_id = string.format("%s:4", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "4" + }) + + initiate_info_changed(relay, "light-binary") + test.socket.matter:__expect_send({ + parent.id, + clusters.OnOff.attributes.OnOff:subscribe(parent, nil) + }) + + initiate_info_changed(child_switch, "light-binary") + test.socket.matter:__expect_send({ + parent.id, + clusters.OnOff.attributes.OnOff:subscribe(parent, nil) + }) + + --Scenario 2: EP4 removed → profile changes to 2-button, child created for EP3 + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(3), + data_types.Uint16(8), + data_types.Uint16(9), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "3" + }) + + relay:expect_metadata_update({ profile = "2-button" }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + + -- Scenario 3: EP4 reappears → profile changes back to light-binary + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(3), + data_types.Uint16(4), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) + }) + relay:expect_metadata_update({ profile = "light-binary" }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "4" + }) + + -- Scenario 4: EP3 removed → profile changes to 2-button, child created for EP4 + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(4), + data_types.Uint16(12), + data_types.Uint16(13), + })) + }) + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 13) + }) + + relay:expect_metadata_update({ profile = "2-button" }) + + -- Scenario 5: EP3 and EP4 removed → 4-button profile + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(10), + data_types.Uint16(11), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 10) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 11) + }) + relay:expect_metadata_update({ profile = "4-button" }) + + -- Scenario 6: Only EP4 present → 2-button profile, child created + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(4), + data_types.Uint16(8), + data_types.Uint16(9), + })) + }) + + relay:expect_metadata_update({ profile = "2-button" }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) + }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "4" + }) +end) + +test.register_coroutine_test("Test: Dimmer Device - Child Creation for Dimmable Endpoint with Button Support", function() + test.socket.matter:__set_channel_ordering("relaxed") + + add_parent_device(parent) + local dimmer = create_hager_dimmer_device_2g("2-button", parent) + add_matter_device(dimmer, parent, "2-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + --Scenario 1: 2 button endpoints (8, 9) detected → 2-button profile + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(3) + })) + }) + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 3) + }) + dimmer:expect_metadata_update({ profile = "2-button" }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 3, data_types.Array { + { + device_type = data_types.Uint32(257), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-level", + parent_device_id = parent.id, + parent_assigned_child_key = "3" + }) + + -- Create mock child device for EP3 + local child_dimmer = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("light-level.yml"), + device_network_id = string.format("%s:3", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "3" + }) + initiate_info_changed(child_dimmer, "light-level") + + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) + }) + + test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) + child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switch", "on") + + test.socket.matter:__expect_send({ + parent.id, + clusters.OnOff.server.commands.On(child_dimmer, 3) + }) + test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 20 } } }) + + child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switchLevel", "setLevel") + + test.socket.matter:__expect_send({ + parent.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(child_dimmer, 3, 51, nil, 0, 0) + }) + +end) + +test.register_coroutine_test("Test: 1G Dimmer - Initialization and Profile Update to light-level", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local dimmer = create_hager_dimmer_device_2g("2-button", parent) + add_matter_device(dimmer, parent, "2-button") + test.wait_for_events() + configure_parent(parent) +end) + +test.register_coroutine_test("Test: 1G Dimmer - Host Commands and Level Control Capabilities", function() + test.socket.matter:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + + add_parent_device(parent_1g) + local dimmer = create_hager_dimmer_device_1g("light-level", parent_1g) + add_matter_device(dimmer, parent_1g, "light-level") + test.wait_for_events() + configure_parent(parent_1g) + + -- Send dimmable endpoint 4 detection (device type 257) to trigger profile change and subscriptions + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_1g, 2, data_types.Array({ + data_types.Uint16(4), + })) + }) + + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_1g, 4) + }) + + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_1g, 4, data_types.Array { + { + device_type = data_types.Uint32(257), + revision = data_types.Uint16(1) + } + }) + }) + + ----subscriptions from device_type_handler + + initiate_info_changed(dimmer, "light-level") + test.socket.matter:__expect_send({ + parent_1g.id, + cluster_base.subscribe(parent_1g, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) + }) + + test.socket.capability:__queue_receive({ dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) + dimmer.expect_native_cmd_handler_registration(dimmer, "switch", "on") + + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.OnOff.server.commands.On(dimmer, 4) + }) + test.socket.capability:__queue_receive({ dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 20 } } }) + + dimmer.expect_native_cmd_handler_registration(dimmer, "switchLevel", "setLevel") + + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(dimmer, 4, 51, nil, 0, 0) + }) +end) + +test.register_coroutine_test("Test: PIR Device - Initialization with Motion and Illuminance Capabilities", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent_pir) + local pir_device = create_hager_pir_device("motion-illuminance", parent_pir) + add_matter_device(pir_device, parent_pir, "motion-illuminance") + test.wait_for_events() + configure_parent(parent_pir) + +end) + +test.register_coroutine_test("Test: PIR Device - Complete Functionality with Motion, Illuminance, and Dimmer Support", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent_pir) + local pir_device = create_hager_pir_device("motion-illuminance", parent_pir) + add_matter_device(pir_device, parent_pir, "motion-illuminance") + test.wait_for_events() + + configure_parent(parent_pir) + + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_pir, 2, data_types.Array({ + data_types.Uint16(3), + data_types.Uint16(4), + data_types.Uint16(5) + + })) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 3) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 4) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 5) + }) + + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 4, data_types.Array { + { + device_type = data_types.Uint32(263), + revision = data_types.Uint16(1) + } + }) + }) + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 5, data_types.Array { + { + device_type = data_types.Uint32(262), + revision = data_types.Uint16(1) + } + }) + }) + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 3, data_types.Array { + { + device_type = data_types.Uint32(257), + revision = data_types.Uint16(1) + } + }) + }) + + parent_pir:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-level", + parent_device_id = parent_pir.id, + parent_assigned_child_key = "3" + }) + test.wait_for_events() + initiate_info_changed(pir_device, "motion-illuminance") + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.IlluminanceMeasurement.ID, clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.OccupancySensing.ID, clusters.OccupancySensing.attributes.Occupancy.ID, nil) + }) + + local child_dimmer = test.mock_device.build_test_child_device({ + label = "Hager G2 4x Button 1", + profile = t_utils.get_profile_definition("light-level.yml"), + device_network_id = string.format("%s:3", parent_pir.id), + parent_device_id = parent_pir.id, + parent_assigned_child_key = "3" + }) + + test.mock_device.add_test_device(child_dimmer) + initiate_info_changed(child_dimmer, "light-level") + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) + }) + test.wait_for_events() + --Verify motion detected event + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(parent_pir, 6, 1) + }) + + test.socket.capability:__expect_send(pir_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + + -- Verify illuminance measurement event + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_report_data(parent_pir, 5, 21370) + }) + test.socket.capability:__expect_send(pir_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance(137))) + + -- Send on command to OnOff endpoint (dimmer) + child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switch", "on") + + test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.OnOff.commands.On(parent_pir, 3) + }) + -- Verify on state via attribute report + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(parent_pir, 3, true) + }) + test.socket.capability:__expect_send(child_dimmer:generate_test_message("main", capabilities.switch.switch.on())) + parent_pir.expect_native_attr_handler_registration(parent_pir, "switch", "switch") + + -- Set dimmer level to 50% + test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 50 } } }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.LevelControl.commands.MoveToLevelWithOnOff(parent_pir, 3, 127, nil, 0, 0) + }) + child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switchLevel", "setLevel") + + -- Verify level via attribute report + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(parent_pir, 3, 127) + }) + test.socket.capability:__expect_send(child_dimmer:generate_test_message("main", capabilities.switchLevel.level(50))) + parent_pir.expect_native_attr_handler_registration(parent_pir, "switchLevel", "level") +end) + +test.register_coroutine_test("Test: Host with Window Covering - 2-Button Profile with Window Covering Child Device", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local window_covering_device = create_matter_device_with_window("2-button", parent) + add_matter_device(window_covering_device, parent, "2-button") + test.wait_for_events() + configure_parent(parent) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(12), + })) + }) + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) + }) + + -- DeviceTypeList report for window covering endpoint (type 514 = Window Covering Device) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { + { + device_type = data_types.Uint32(514), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "window-covering", + parent_device_id = parent.id, + parent_assigned_child_key = "12" + }) + + local child_wc = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("window-covering.yml"), + device_network_id = string.format("%s:12", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "12" + }) + + test.mock_device.add_test_device(child_wc) + initiate_info_changed(child_wc, "window-covering") + + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) + }) + + -- Open command + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "open", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.UpOrOpen(parent, 12) }) + test.wait_for_events() + + -- Verify open state via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.OperationalStatus:build_test_report_data(parent, 12, 0x01) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.opening())) + + + -- Close command + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "close", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.DownOrClose(parent, 12) }) + + -- Verify close state via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.OperationalStatus:build_test_report_data(parent, 12, 0x02) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.closing())) + + -- Pause command + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "pause", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.StopMotion(parent, 12) }) + test.wait_for_events() + + -- Set shade level to 50% + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 } } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 5000, nil, 0, 0) }) + test.wait_for_events() + + -- Verify shade level via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 5000) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.partially_open())) + + + -- Set shade level to 100% + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 100 } } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 0, nil, 0, 0) }) + test.wait_for_events() + + -- Verify shade level via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 0) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.open())) + + -- Set shade level to 0% + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 0 } } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 10000, nil, 0, 0) }) + + -- Verify shade level via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 10000) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.closed())) + +end) + +test.register_coroutine_test("Test: Window Covering - Preference Changes for Reverse Polarity and Preset Position", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local window_covering_device = create_matter_device_with_window("2-button", parent) + add_matter_device(window_covering_device, parent, "2-button") + test.wait_for_events() + configure_parent(parent) + + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(12), + })) + }) + -- + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) + }) + test.wait_for_events() + + -- DeviceTypeList reports for button endpoints (type 15 = Generic Switch) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + + -- DeviceTypeList report for window covering endpoint (type 514 = Window Covering Device) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { + { + device_type = data_types.Uint32(514), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "window-covering", + parent_device_id = parent.id, + parent_assigned_child_key = "12" + }) + + local child_wc = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("window-covering.yml"), + device_network_id = string.format("%s:12", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "12" + }) + test.mock_device.add_test_device(child_wc) + + test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { reverse = "false" } })) + test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { reverse = "true" } })) + test.wait_for_events() + local reverse_preference_set = child_wc.preferences.reverse + assert(reverse_preference_set == "true", "reverse_preference_set is True") + -- + --Send open command - with reverse_polarity true, this should send DownOrClose + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "open", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.DownOrClose(parent, 12) }) + + -- Send close command - with reverse_polarity true, this should send UpOrOpen + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "close", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.UpOrOpen(parent, 12) }) + + -- Send Pause command + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "pause", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.StopMotion(parent, 12) }) + + -- Position preset testing + test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { presetPosition = "50" } })) + test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { presetPosition = "20" } })) + + test.wait_for_events() + + local PRESET_LEVEL_KEY = child_wc.preferences.presetPosition + assert(PRESET_LEVEL_KEY == "20", " presetPosition is set to 20") + + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } }) + test.socket.matter:__expect_send( + { parent.id, clusters.WindowCovering.server.commands.GoToLiftPercentage(parent, 12, 8000) } + ) + + test.wait_for_events() + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + })) + }) + + test.wait_for_events() + + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + test.mock_time.advance_time(5) + local BUTTON_EPS_FIELD = parent:get_field(BUTTON_EPS) + assert(BUTTON_EPS_FIELD[1] == 8) + assert(BUTTON_EPS_FIELD[2] == 9) + + test.wait_for_events() + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(12), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { + { + device_type = data_types.Uint32(514), + revision = data_types.Uint16(1) + } + }) + }) + child_wc:expect_metadata_update({ profile = "window-covering" }) + initiate_info_changed(child_wc, "window-covering") + + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) + }) +end) + +test.run_registered_tests() From aa3836966a4c37933d91b342656cd4ab6a60aca4 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:44:26 -0500 Subject: [PATCH 19/24] Merge pull request #3085 from JanJakubiszyn/revert-2858-Hager_WAASYS_device_support Revert "Adding support for Hager WAASYS devices" --- .../matter-switch/fingerprints.yml | 20 - .../profiles/motion-illuminance.yml | 14 - .../profiles/window-covering.yml | 21 - .../SmartThings/matter-switch/src/init.lua | 3 +- .../src/sub_drivers/hager/can_handle.lua | 13 - .../src/sub_drivers/hager/init.lua | 475 ----- .../switch_handlers/attribute_handlers.lua | 2 +- .../src/switch_utils/device_configuration.lua | 1 + .../matter-switch/src/switch_utils/fields.lua | 10 - .../src/test/test_hager_waasys.lua | 1776 ----------------- 10 files changed, 3 insertions(+), 2332 deletions(-) delete mode 100644 drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml delete mode 100644 drivers/SmartThings/matter-switch/profiles/window-covering.yml delete mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua delete mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua delete mode 100644 drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index d6a367c685..930e38e559 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -909,26 +909,6 @@ matterManufacturer: vendorId: 0x1285 productId: 0x0009 deviceProfileName: 4-button-batteryLevel - - id: "4741/5" - deviceLabel: Hager Switch 1G - vendorId: 0x1285 - productId: 0x0005 - deviceProfileName: matter-bridge - - id: "4741/6" - deviceLabel: Hager Switch 2G - vendorId: 0x1285 - productId: 0x0006 - deviceProfileName: matter-bridge - - id: "4741/7" - deviceLabel: Hager PIR 1.1M - vendorId: 0x1285 - productId: 0x0007 - deviceProfileName: matter-bridge - - id: "4741/10" - deviceLabel: Hager PIR 2.2M - vendorId: 0x1285 - productId: 0x000A - deviceProfileName: matter-bridge # HAOJAI - id: "5530/4113" deviceLabel: HAOJAI Smart Switch 3-key diff --git a/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml b/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml deleted file mode 100644 index dfe052043c..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: motion-illuminance -components: -- id: main - capabilities: - - id: motionSensor - version: 1 - - id: illuminanceMeasurement - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: MotionSensor diff --git a/drivers/SmartThings/matter-switch/profiles/window-covering.yml b/drivers/SmartThings/matter-switch/profiles/window-covering.yml deleted file mode 100644 index b588b41ec9..0000000000 --- a/drivers/SmartThings/matter-switch/profiles/window-covering.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: window-covering -components: -- id: main - capabilities: - - id: windowShade - version: 1 - - id: windowShadePreset - version: 1 - - id: windowShadeLevel - version: 1 - - id: firmwareUpdate - version: 1 - - id: refresh - version: 1 - categories: - - name: Blind -preferences: - - preferenceId: presetPosition - explicit: true - - preferenceId: reverse - explicit: true diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 31229b7838..577820c171 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -1,4 +1,4 @@ --- Copyright © 2026 SmartThings, Inc. +-- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 local MatterDriver = require "st.matter.driver" @@ -374,7 +374,6 @@ local matter_driver_template = { switch_utils.lazy_load_if_possible("sub_drivers.aqara_cube"), switch_utils.lazy_load("sub_drivers.camera"), switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), - switch_utils.lazy_load_if_possible("sub_drivers.hager"), switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_garage_door"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua deleted file mode 100644 index 3ac75a5805..0000000000 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua +++ /dev/null @@ -1,13 +0,0 @@ --- Copyright © 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -return function(opts, driver, device, ...) - local device_lib = require "st.device" - local get_product_override_field = require "switch_utils.utils".get_product_override_field - - local checked_device = device.network_type == device_lib.NETWORK_TYPE_MATTER and device or device:get_parent_device() - if get_product_override_field(checked_device, "needs_hager_subdriver") then - return true, require("sub_drivers.hager") - end - return false -end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua deleted file mode 100644 index 448295247d..0000000000 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua +++ /dev/null @@ -1,475 +0,0 @@ --- Copyright © 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" -local cluster_base = require "st.matter.cluster_base" -local device_lib = require "st.device" -local buttonCfg = require "switch_utils.device_configuration".ButtonCfg -local create_child = require "switch_utils.device_configuration".ChildCfg.create_or_update_child_devices -local switch_utils = require "switch_utils.utils" -local fields = require "switch_utils.fields" - -local ACTIVE_EPS = "__active_EPS" -local MATTER_DEVICE_ID = "MATTER_DEVICE_ID" -local PARENT_ID = "PARENT_ID" -local CURRENT_LIFT = "__current_lift" -local BUTTON_EPS = "__button_eps" -local MAIN_WC_EP = "__main_wc_ep" - -local PRODUCT_ID = { - SWITCH_1G = 0x0005, - SWITCH_2G = 0x0006, - PIR_1_1M = 0x0007, - PIR_2_2M = 0x000A, -} - -local function subscribe (device, endpoint_id, cluster_id, attr_id, event_id) - device:send(cluster_base.subscribe(device, endpoint_id, cluster_id, attr_id, event_id)) -end - -local function get_parent (driver, device) - return driver:get_device_info(device:get_field(PARENT_ID) or nil) -end - -local function get_matter_device(device) - local matter_device_id = device:get_field(MATTER_DEVICE_ID) - if matter_device_id then - local driver = device.driver - if driver then - local matter_device = driver:get_device_info(matter_device_id) - if matter_device then - return matter_device - end - end - end - return nil -end - -local function extract_reported_endpoints(ib) - local eps = {} - if ib.data and ib.data.elements then - for _, el in ipairs(ib.data.elements) do - local ep_id = el.value - if type(ep_id) == "number" and ep_id ~= 1 and ep_id ~= 2 then - table.insert(eps, ep_id) - end - end - end - return eps -end - -local function detect_endpoint_changes (device, ib_elements) - local stored_eps = device:get_field(ACTIVE_EPS) or {} - ib_elements = ib_elements or {} - - local old_set, new_set = {}, {} - for _, ep_id in ipairs(stored_eps) do - old_set[ep_id] = true - end - for _, ep_id in ipairs(ib_elements) do - new_set[ep_id] = true - end - - local removed_eps, added_eps = {}, {} - - for ep_id in pairs(old_set) do - if not new_set[ep_id] then - table.insert(removed_eps, ep_id) - end - end - for ep_id in pairs(new_set) do - if not old_set[ep_id] then - table.insert(added_eps, ep_id) - end - end - return removed_eps, added_eps -end - -local function assign_profile_for_endpoint(device_type_id) - local profile = fields.device_type_profile_map[device_type_id] or "switch-binary" - return function(device, ep_id, is_child_device) - return profile, nil - end -end - -local function link_matter_device_and_parent(matter_device) - local parent = matter_device:get_parent_device() - matter_device:set_field(PARENT_ID, parent.id, { persist = true }) - matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) - parent:set_field(PARENT_ID, parent.id, { persist = true }) - parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) -end - -local function handle_set_preset(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local lift_value = device.preferences.presetPosition - local hundredths_lift_percent = (100 - tonumber(lift_value)) * 100 - device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage(device, endpoint_id, hundredths_lift_percent)) -end - -local function handle_close(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) - if device.preferences.reverse then - req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) - end - device:send(req) -end - -local function handle_open(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) - if device.preferences.reverse then - req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) - end - device:send(req) -end - -local function handle_pause(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local req = clusters.WindowCovering.server.commands.StopMotion(device, endpoint_id) - device:send(req) -end - -local function handle_shade_level(driver, device, cmd) - local endpoint_id = device:component_to_endpoint(cmd.component) - local lift_percentage_value = 100 - cmd.args.shadeLevel - local hundredths_lift_percentage = lift_percentage_value * 100 - local req = clusters.WindowCovering.server.commands.GoToLiftPercentage( - device, endpoint_id, hundredths_lift_percentage - ) - device:send(req) -end - -local current_pos_handler = function(attribute) - return function(driver, device, ib, response) - if ib.data.value == nil then - return - end - local windowShade = capabilities.windowShade.windowShade - local position = 100 - math.floor(ib.data.value / 100) - local reverse = device.preferences.reverse - device:emit_event_for_endpoint(ib.endpoint_id, attribute(position)) - if attribute == capabilities.windowShadeLevel.shadeLevel then - device:set_field(CURRENT_LIFT, position) - end - local lift_position = device:get_field(CURRENT_LIFT) - if lift_position == 100 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closed() or windowShade.open()) - elseif lift_position > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) - elseif lift_position == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) - end - end -end - -local function current_status_handler(driver, device, ib, response) - local windowShade = capabilities.windowShade.windowShade - local reverse = device.preferences.reverse - local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL - if state == 1 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closing() or windowShade.opening()) - elseif state == 2 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.opening() or windowShade.closing()) - elseif state ~= 0 then - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) - end -end - -local function info_changed(driver, device, event, args) - if not device or not device.id or not device.profile then - return - end - - if device.profile.id ~= args.old_st_store.profile.id or device.network_type == device_lib.NETWORK_TYPE_CHILD then - local parent = device:get_parent_device() - local matter_device = get_matter_device(parent) - local map = {} - device.thread:call_with_delay(2, function() - if device:supports_capability(capabilities.button) then - local button_eps = parent:get_field(BUTTON_EPS) - local clean_eps = {} - table.sort(button_eps) - - for _, v in ipairs(button_eps or {}) do - table.insert(clean_eps, v) - end - - buttonCfg.update_button_component_map(matter_device, clean_eps[1], clean_eps) - for _, ep_id in ipairs(clean_eps) do - subscribe(device, ep_id, clusters.Switch.ID, nil, clusters.Switch.events.MultiPressComplete.ID) - subscribe(device, ep_id, clusters.Switch.ID, nil, clusters.Switch.events.LongPress.ID) - device:emit_event_for_endpoint(ep_id, capabilities.button.supportedButtonValues({ "pushed", "double", "held" })) - end - return - elseif device:supports_capability(capabilities.switch) then - map = { main = 3 } - subscribe(device, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) - if device:supports_capability(capabilities.switchLevel) then - map = { main = 4 } - subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) - subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID) - subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID) - end - elseif device:supports_capability(capabilities.motionSensor) then - subscribe(device, nil, clusters.OccupancySensing.ID, clusters.OccupancySensing.attributes.Occupancy.ID, nil) - subscribe(device, nil, clusters.IlluminanceMeasurement.ID, clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID, nil) - elseif device:supports_capability(capabilities.windowShadeLevel) then - map = { main = 5 } - subscribe(device, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID) - subscribe(device, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) - end - if device.id == matter_device.id then - parent:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, map, { persist = true }) - matter_device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, map, { persist = true }) - end - end) - end -end - -local function device_init (driver, device) - if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then - device.thread:call_with_delay(4, function() - info_changed(driver, device, nil, { - old_st_store = { profile = { } } - }) - end) - return - end - - if device:get_parent_device() ~= nil then - link_matter_device_and_parent(device) - local parent = device:get_parent_device() - device:extend_device("send", function(self, message) - return parent:send(message) - end) - else - subscribe(device, 2, clusters.Descriptor.ID, clusters.Descriptor.attributes.PartsList.ID) - device.thread:call_with_delay(3, function() - local matter_device = get_matter_device(device) - device:extend_device("emit_event_for_endpoint", function(self, ep_info, event) - local endpoint_id = type(ep_info) == "number" and ep_info or ep_info.endpoint_id - local child = self:get_child_by_parent_assigned_key(string.format("%d", endpoint_id)) - if child then - return child:emit_event(event) - end - if matter_device then - return matter_device:emit_event_for_endpoint(ep_info, event) - end - end) - end) - end - - device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) - device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) -end - -local function handle_descriptor_report(driver, device, ib, response) - local product_id = device.manufacturer_info and device.manufacturer_info.product_id - local parent = get_parent(driver, device) - - if not parent or ib.endpoint_id ~= 2 then - return - end - - local matter_device = get_matter_device(device) - - local new_eps = extract_reported_endpoints(ib) or {} - table.sort(new_eps) - local removed_eps, added_eps = detect_endpoint_changes(device, new_eps) - - device:set_field(ACTIVE_EPS, new_eps, { persist = true }) - - for _, ep_id in ipairs(added_eps or {}) do - subscribe(parent, ep_id, clusters.Descriptor.ID, clusters.Descriptor.attributes.DeviceTypeList.ID, nil) - - if product_id == PRODUCT_ID.SWITCH_1G and ep_id == 3 then - matter_device:try_update_metadata({ profile = "light-binary" }) - elseif product_id == PRODUCT_ID.SWITCH_2G then - if ep_id == 3 then - local profile = switch_utils.tbl_contains(new_eps, 4) and "light-binary" or "2-button" - matter_device:try_update_metadata({ profile = profile }) - elseif ep_id == 4 then - local profile = switch_utils.tbl_contains(new_eps, 3) and "light-binary" or "2-button" - matter_device:try_update_metadata({ profile = profile }) - end - end - end - - for _, ep_id in ipairs(removed_eps) do - local button_eps = parent:get_field(BUTTON_EPS) or {} - local clean_eps = {} - - for _, value in ipairs(button_eps) do - if value ~= ep_id then - table.insert(clean_eps, value) - end - end - table.sort(clean_eps) - parent:set_field(BUTTON_EPS, clean_eps, { persist = true }) - matter_device:set_field(BUTTON_EPS, clean_eps, { persist = true }) - - local child = parent:get_child_by_parent_assigned_key(tostring(ep_id)) - if child then - driver:try_delete_device(child.id) - end - - if ep_id == 3 then - if device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then - matter_device:try_update_metadata({ profile = "2-button" }) - elseif device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then - local has_ep4 = switch_utils.tbl_contains(new_eps, 4) - matter_device:try_update_metadata({ profile = has_ep4 and "2-button" or "4-button" }) - end - elseif ep_id == 4 then - if device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then - local button_comb = switch_utils.tbl_contains(new_eps, 3) - if button_comb then - matter_device:try_update_metadata({ profile = "2-button" }) - create_child(driver, parent, { 3 }, 1, assign_profile_for_endpoint(fields.DEVICE_TYPE_ID.LIGHT.ON_OFF)) - else - matter_device:try_update_metadata({ profile = "4-button" }) - end - end - end - end -end - -local function device_type_handler (driver, device, ib) - local parent = get_parent(driver, device) - if not parent then - return - end - - local matter_device = get_matter_device(device) - local stored_btn_eps = parent:get_field(BUTTON_EPS) or {} - local new_btn_eps = {} - local ep_id = ib.endpoint_id - local value = ib.data.elements - - for _, v in ipairs(stored_btn_eps) do - table.insert(new_btn_eps, v) - end - for _, element in ipairs(value) do - local device_type_field = element.elements.device_type - local device_type_id = device_type_field and device_type_field.value - - if device_type_id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH then - if not switch_utils.tbl_contains(new_btn_eps, ep_id) then - switch_utils.set_field_for_endpoint(parent, fields.SUPPORTS_MULTI_PRESS, ep_id) - switch_utils.set_field_for_endpoint(parent, fields.IGNORE_NEXT_MPC, ep_id) - table.insert(new_btn_eps, ep_id) - table.sort(new_btn_eps) - parent:set_field(BUTTON_EPS, new_btn_eps, { persist = true }) - end - end - - if device_type_id == fields.DEVICE_TYPE_ID.LIGHT.ON_OFF then - local active_eps = device:get_field(ACTIVE_EPS) - - if ep_id == 3 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then - return - elseif ep_id == 4 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then - if switch_utils.tbl_contains(active_eps, 3) then - local ep3 = parent:get_child_by_parent_assigned_key("3") - if ep3 then - driver:try_delete_device(ep3.id) - end - else - create_child(driver, parent, { 4 }, 1, assign_profile_for_endpoint(device_type_id)) - return - end - elseif ep_id == 3 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then - if not switch_utils.tbl_contains(active_eps, 4) then - create_child(driver, parent, { 3 }, 1, assign_profile_for_endpoint(device_type_id)) - else - driver:try_delete_device(ep_id) - end - return - end - create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) - elseif device_type_id == fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE then - if ep_id == 4 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then - return - end - create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) - elseif device_type_id == fields.DEVICE_TYPE_ID.WINDOW_COVERING then - if matter_device:get_field(MAIN_WC_EP) == nil then - create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) - end - end - end -end - -local function do_configure (driver, device) - if device.network_type == device_lib.NETWORK_TYPE_MATTER then - - local wc_eps = device:get_endpoints(clusters.WindowCovering.ID) - local oc_eps = device:get_endpoints(clusters.OccupancySensing.ID) - local bt_eps = device:get_endpoints(clusters.Switch.ID) - local lvl_eps = device:get_endpoints(clusters.LevelControl.ID) - local product_id = device.manufacturer_info.product_id - - table.sort(wc_eps) - table.sort(oc_eps) - table.sort(lvl_eps) - - if #oc_eps > 0 then - device:try_update_metadata({ profile = "motion-illuminance" }) - elseif #wc_eps > 0 and product_id == PRODUCT_ID.SWITCH_1G then - device:try_update_metadata({ profile = "window-covering" }) - device:set_field(MAIN_WC_EP, wc_eps[1], { persist = true }) - elseif #bt_eps == 4 then - device:try_update_metadata({ profile = "4-button" }) - elseif #bt_eps == 2 then - device:try_update_metadata({ profile = "2-button" }) - elseif #lvl_eps > 0 and product_id == PRODUCT_ID.SWITCH_1G then - device:try_update_metadata({ profile = "light-level" }) - end - device:set_field(BUTTON_EPS, bt_eps, { persist = true }) - end -end - -local function added (driver, device) end -local function driver_switched(driver, device) end - -local hager_switch = { - NAME = "Hager Subdriver", - lifecycle_handlers = { - added = added, - init = device_init, - infoChanged = info_changed, - doConfigure = do_configure, - driverSwitched = driver_switched - }, - matter_handlers = { - attr = { - [clusters.Descriptor.ID] = { - [clusters.Descriptor.attributes.PartsList.ID] = handle_descriptor_report, - [clusters.Descriptor.attributes.DeviceTypeList.ID] = device_type_handler, - }, - [clusters.WindowCovering.ID] = { - [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_pos_handler(capabilities.windowShadeLevel.shadeLevel), - [clusters.WindowCovering.attributes.OperationalStatus.ID] = current_status_handler, - }, - }, - }, - capability_handlers = { - [capabilities.windowShadePreset.ID] = { - [capabilities.windowShadePreset.commands.presetPosition.NAME] = handle_set_preset, - }, - [capabilities.windowShade.ID] = { - [capabilities.windowShade.commands.close.NAME] = handle_close, - [capabilities.windowShade.commands.open.NAME] = handle_open, - [capabilities.windowShade.commands.pause.NAME] = handle_pause, - }, - [capabilities.windowShadeLevel.ID] = { - [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = handle_shade_level, - }, - }, - can_handle = require("sub_drivers.hager.can_handle") -} - -return hager_switch diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index b7feed6344..45694bc613 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -246,7 +246,7 @@ end -- [[ OCCUPANCY CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.occupancy_handler(driver, device, ib, response) - device:emit_event_for_endpoint(ib.endpoint_id, ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) + device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) 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 9c5235d1e2..772669ebd7 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -156,6 +156,7 @@ function ButtonDeviceConfiguration.update_button_component_map(device, default_e device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) end + function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) local msr_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE}) local msl_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index 5873cb40b2..a8e9781172 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -34,9 +34,7 @@ SwitchFields.DEVICE_TYPE_ID = { ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, GENERIC_SWITCH = 0x000F, - LIGHT_SENSOR = 0x0106, IRRIGATION_SYSTEM = 0x0040, - OCCUPANCY_SENSOR = 0x0107, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, ON_OFF_PLUG_IN_UNIT = 0x010A, @@ -52,7 +50,6 @@ SwitchFields.DEVICE_TYPE_ID = { COLOR_DIMMER = 0x0105, }, WATER_VALVE = 0x0042, - WINDOW_COVERING = 0x0202 } SwitchFields.device_type_profile_map = { @@ -67,7 +64,6 @@ SwitchFields.device_type_profile_map = { [SwitchFields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT] = "plug-level", [SwitchFields.DEVICE_TYPE_ID.MOUNTED_ON_OFF_CONTROL] = "switch-binary", [SwitchFields.DEVICE_TYPE_ID.MOUNTED_DIMMABLE_LOAD_CONTROL] = "switch-level", - [SwitchFields.DEVICE_TYPE_ID.WINDOW_COVERING] = "window-covering" } -- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for @@ -120,12 +116,6 @@ SwitchFields.vendor_overrides = { [0x1189] = { -- LEDVANCE_MANUFACTURER_ID [0x0891] = { target_profile = "switch-binary", initial_profile = "light-binary" }, }, - [0x1285] = { -- HAGER_MANUFACTURER_ID - [0x0005] = { needs_hager_subdriver = true }, -- Hager HBnet 1g switch - [0x0006] = { needs_hager_subdriver = true }, -- Hager HBnet 2g switch - [0x0007] = { needs_hager_subdriver = true }, -- Hager HBnet PIR 1.1M - [0x000A] = { needs_hager_subdriver = true }, -- Hager HBnet PIR 2.2M - }, [0x1321] = { -- SONOFF_MANUFACTURER_ID [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, diff --git a/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua deleted file mode 100644 index 2a1652a2be..0000000000 --- a/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua +++ /dev/null @@ -1,1776 +0,0 @@ --- Copyright © 2026 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local test = require "integration_test" -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" -local data_types = require "st.matter.data_types" -local clusters = require "st.matter.clusters" -local cluster_base = require "st.matter.cluster_base" -local descriptor = require "st.matter.generated.zap_clusters.Descriptor" - -local MATTER_DEVICE_ID = "MATTER_DEVICE_ID" -local PARENT_ID = "PARENT_ID" -local BUTTON_EPS = "__button_eps" - -local function create_parent_device(product_id) - return test.mock_device.build_test_matter_device({ - label = "Hager G2 4x Button", - profile = t_utils.get_profile_definition("matter-bridge.yml"), - manufacturer_info = { - vendor_id = 0x1285, - product_id = product_id, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, - device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } - }, - { - endpoint_id = 1, - clusters = { - { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } - }, - device_types = { { device_type_id = 0x000E, device_type_revision = 1 } } -- AggregateNode - }, - { - endpoint_id = 2, - clusters = { - { - cluster_id = clusters.Descriptor.ID, - cluster_type = "SERVER", - cluster_revision = 1, - } - }, - device_types = { { device_type_id = 0x0039, device_type_revision = 1 } } -- BridgedNode - } - } - }) -end - -local function create_matter_device(profile_name, parent) - return test.mock_device.build_test_matter_device({ - label = "Hager G2 4x Button", - profile = t_utils.get_profile_definition(profile_name .. ".yml"), - type = "MATTER", - manufacturer_info = { - vendor_id = 0x1285, - product_id = 0x0006, - }, - parent_device_id = parent.id, - endpoints = { - { - endpoint_id = 0, - clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, - device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } - }, - { - endpoint_id = 8, - clusters = { - { - cluster_id = clusters.Switch.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - attributes = { - [clusters.Switch.attributes.MultiPressMax.ID] = 2 - } - } - }, - device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } - }, - { - endpoint_id = 9, - clusters = { - { - cluster_id = clusters.Switch.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - attributes = { - [clusters.Switch.attributes.MultiPressMax.ID] = 2 - } - } - }, - device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } - }, - { - endpoint_id = 10, - clusters = { - { - cluster_id = clusters.Switch.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - attributes = { - [clusters.Switch.attributes.MultiPressMax.ID] = 2 - } - } - }, - device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } - }, - { - endpoint_id = 11, - clusters = { - { - cluster_id = clusters.Switch.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - attributes = { - [clusters.Switch.attributes.MultiPressMax.ID] = 2 - } - } - }, - device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } - }, - } - }) -end - --- Create Hager 2G Relay device with endpoints 3 & 4 (OnOff clusters) -local function create_hager_2g_relay(profile_name, parent) - return test.mock_device.build_test_matter_device({ - label = "Hager G2 2G Relay", - profile = t_utils.get_profile_definition(profile_name .. ".yml"), - type = "MATTER", - manufacturer_info = { - vendor_id = 0x1285, - product_id = 0x0006, - }, - parent_device_id = parent.id, - endpoints = { - { - endpoint_id = 0, - clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, - device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } - }, - { - endpoint_id = 3, - clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.OnOff.attributes.OnOff.ID] = false - } - } - }, - device_types = { { device_type_id = 0x0100, device_type_revision = 1 } } - }, - { - endpoint_id = 4, - clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.OnOff.attributes.OnOff.ID] = false - } - } - }, - device_types = { { device_type_id = 0x0100, device_type_revision = 1 } } - }, - } - }) -end - --- Create Hager Dimmer device with ONLY dimmable endpoint (3) - no button endpoints -local function create_hager_dimmer_device_1g(profile_name, parent) - return test.mock_device.build_test_matter_device({ - label = "Hager G2 Dimmer", - profile = t_utils.get_profile_definition(profile_name .. ".yml"), - type = "MATTER", - manufacturer_info = { - vendor_id = 0x1285, - product_id = 0x0005, - }, - parent_device_id = parent.id, - endpoints = { - { - endpoint_id = 0, - clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, - device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } - }, - { - endpoint_id = 4, - clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.OnOff.attributes.OnOff.ID] = false - } - }, - { - cluster_id = clusters.LevelControl.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 - } - } - }, - device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } - }, - } - }) -end - -local function create_hager_dimmer_device_2g(profile_name, parent) - return test.mock_device.build_test_matter_device({ - label = "Hager G2 Dimmer", - profile = t_utils.get_profile_definition(profile_name .. ".yml"), - manufacturer_info = { - vendor_id = 0x1285, - product_id = 0x0006, - }, - parent_device_id = parent.id, - endpoints = { - { - endpoint_id = 0, - clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, - device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } - }, - { - endpoint_id = 3, - clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.OnOff.attributes.OnOff.ID] = false - } - }, - { - cluster_id = clusters.LevelControl.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 - } - } - }, - device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } - }, - { - endpoint_id = 8, - clusters = { - { - cluster_id = clusters.Switch.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - attributes = { - [clusters.Switch.attributes.MultiPressMax.ID] = 2 - } - } - }, - device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } - }, - { - endpoint_id = 9, - clusters = { - { - cluster_id = clusters.Switch.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - attributes = { - [clusters.Switch.attributes.MultiPressMax.ID] = 2 - } - } - }, - device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } - }, - } - }) -end - -local function create_matter_device_with_window(profile_name, parent) - return test.mock_device.build_test_matter_device({ - label = "Hager G2 matter device with Window Covering", - profile = t_utils.get_profile_definition(profile_name .. ".yml"), - manufacturer_info = { - vendor_id = 0x1285, - product_id = 0x0006, - }, - parent_device_id = parent.id, - endpoints = { - { - endpoint_id = 0, - clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, - device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } - }, - { - endpoint_id = 8, - clusters = { - { - cluster_id = clusters.Switch.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - attributes = { - [clusters.Switch.attributes.MultiPressMax.ID] = 2 - } - } - }, - device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } - }, - { - endpoint_id = 9, - clusters = { - { - cluster_id = clusters.Switch.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, - attributes = { - [clusters.Switch.attributes.MultiPressMax.ID] = 2 - } - } - }, - device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } - }, - { - endpoint_id = 12, - clusters = { - { - cluster_id = clusters.WindowCovering.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = clusters.WindowCovering.types.Feature.LIFT | - clusters.WindowCovering.types.Feature.POSITION_AWARE_LIFT | - clusters.WindowCovering.types.Feature.ABSOLUTE_POSITION, - attributes = { - [clusters.WindowCovering.attributes.OperationalStatus.ID] = 0x00, - [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = 0x0000, - } - } - }, - device_types = { { device_type_id = 0x0202, device_type_revision = 1 } } - }, - } - }) -end - -local function create_hager_pir_device(profile_name, parent) - return test.mock_device.build_test_matter_device({ - label = "Hager G2 PIR with Buttons and Motion/Illuminance/Dimmer", - profile = t_utils.get_profile_definition(profile_name .. ".yml"), - type = "MATTER", - manufacturer_info = { - vendor_id = 0x1285, - product_id = 0x0007, - }, - parent_device_id = parent.id, - endpoints = { - { - endpoint_id = 0, - clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, - device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } - }, - { - endpoint_id = 3, - clusters = { - { - cluster_id = clusters.OnOff.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.OnOff.attributes.OnOff.ID] = false - } - }, - { - cluster_id = clusters.LevelControl.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 - } - } - }, - device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } - }, - { - endpoint_id = 6, - clusters = { - { - cluster_id = clusters.OccupancySensing.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.OccupancySensing.attributes.Occupancy.ID] = 0 - } - } - }, - device_types = { { device_type_id = 0x0107, device_type_revision = 1 } } - }, - { - endpoint_id = 5, - clusters = { - { - cluster_id = clusters.IlluminanceMeasurement.ID, - cluster_type = "SERVER", - cluster_revision = 1, - attributes = { - [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = 0 - } - } - }, - device_types = { { device_type_id = 0x0106, device_type_revision = 1 } } - }, - } - }) -end - -local function add_parent_device(parent) - test.mock_device.add_test_device(parent) - test.socket.device_lifecycle:__queue_receive({ parent.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ parent.id, "init" }) - test.mock_time.advance_time(5) - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, 2, descriptor.ID, descriptor.attributes.PartsList.ID, nil) - }) -end - -local function subscribe_switch_events(parent, button_eps) - for _, ep in ipairs(button_eps) do - test.socket.matter:__expect_send({ - parent.id, - clusters.Switch.events.MultiPressComplete:subscribe(parent, ep) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Switch.events.LongPress:subscribe(parent, ep) - }) - end -end - -local function add_matter_device(matter_device, parent, expected_profile_change) - test.mock_device.add_test_device(matter_device) - test.socket.device_lifecycle:__queue_receive({ matter_device.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ matter_device.id, "doConfigure" }) - matter_device:expect_metadata_update({ profile = expected_profile_change }) - matter_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.device_lifecycle:__queue_receive({ matter_device.id, "init" }) - - matter_device:set_field(PARENT_ID, parent.id, { persist = true }) - matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) - parent:set_field(PARENT_ID, parent.id, { persist = true }) - parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) - parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) -end - -local function add_switch_matter_device(matter_device, parent) - test.mock_device.add_test_device(matter_device) - test.socket.device_lifecycle:__queue_receive({ matter_device.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ matter_device.id, "doConfigure" }) - matter_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.device_lifecycle:__queue_receive({ matter_device.id, "init" }) - - matter_device:set_field(PARENT_ID, parent.id, { persist = true }) - matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) - parent:set_field(PARENT_ID, parent.id, { persist = true }) - parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) - parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) -end - -local function button_supported_values (matter_device) - test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) - test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) - test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) - test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) -end - -local function initiate_info_changed(device, profile) - test.socket.device_lifecycle:__queue_receive(device:generate_info_changed({ profile = { id = profile } })) - test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") - test.mock_time.advance_time(3) -end - -local function configure_parent(device) - test.socket.device_lifecycle:__queue_receive({ device.id, "doConfigure" }) - device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.socket.device_lifecycle:__queue_receive({ device.id, "init" }) - test.socket.matter:__expect_send({ - device.id, - cluster_base.subscribe(device, 2, descriptor.ID, descriptor.attributes.PartsList.ID, nil) - }) - test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") - test.mock_time.advance_time(3) -end - -local parent = create_parent_device(0x0006) -- 2G button product -local parent_1g = create_parent_device(0x0005) -- 1G button product -local parent_pir = create_parent_device(0x0007) -- PIR product - -local function test_init() - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test("Test: 4-Button Device Detection - Profile Changes from matter-bridge to 4-button When Four Button Endpoints Present", function() - test.socket.matter:__set_channel_ordering("relaxed") - add_parent_device(parent) - local matter_device = create_matter_device("matter-bridge", parent) - add_matter_device(matter_device, parent, "4-button") - test.wait_for_events() - configure_parent(parent) - test.wait_for_events() - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(8), - data_types.Uint16(9), - data_types.Uint16(10), - data_types.Uint16(11), - })) - }) - - local matter_device_parent_id = matter_device:get_field(PARENT_ID) - local matter_device_id = matter_device:get_field(MATTER_DEVICE_ID) - local parent_id = parent:get_field(PARENT_ID) - local parent_matter_device_id = parent:get_field(MATTER_DEVICE_ID) - - assert(matter_device_parent_id == parent.id, "link_host_and_subhub 1/4") - assert(matter_device_id == matter_device.id, "link_host_and_subhub 2/4") - assert(parent_id == parent.id, "link_host_and_subhub 3/4") - assert(parent_matter_device_id == matter_device.id, "link_host_and_subhub 4/4") - - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x08) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x09) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0A) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0B) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array({ - { device_type = 0x000F, revision = 0x0003 } - })) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array({ - { device_type = 0x000F, revision = 0x0003 } - })) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 10, data_types.Array({ - { device_type = 0x000F, revision = 0x0003 } - })) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 11, data_types.Array({ - { device_type = 0x000F, revision = 0x0003 } - })) - }) - -end) - -test.register_coroutine_test("Test: Button Event Handling - Pushed, Double Press, and Held Events on 4-Button Device", function() - test.socket.matter:__set_channel_ordering("relaxed") - add_parent_device(parent) - local matter_device = create_matter_device("4-button", parent) - add_matter_device(matter_device, parent, "4-button") - test.wait_for_events() - configure_parent(parent) - test.wait_for_events() - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(8), - data_types.Uint16(9), - data_types.Uint16(10), - data_types.Uint16(11), - })) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x08) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x09) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0A) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0B) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array({ - { device_type = 0x000F, revision = 0x0003 } - })) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array({ - { device_type = 0x000F, revision = 0x0003 } - })) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 10, data_types.Array({ - { device_type = 0x000F, revision = 0x0003 } - })) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 11, data_types.Array({ - { device_type = 0x000F, revision = 0x0003 } - })) - }) - test.wait_for_events() - initiate_info_changed(matter_device, "4-button") - - subscribe_switch_events(parent, { 8, 9, 10, 11 }) - button_supported_values(matter_device) - - - -- Test single press (pushed) on endpoint 8 (button1) - test.wait_for_events() - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - matter_device, 8, { - new_position = 1, - previous_position = 1, - total_number_of_presses_counted = 1 -- Single press - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true }))) - - --Test double press on endpoint 8 (button1) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - matter_device, 8, { - new_position = 1, - previous_position = 1, - total_number_of_presses_counted = 2 - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.double({ state_change = true }))) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - matter_device, 8, { - new_position = 1, - previous_position = 0, - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.held({ state_change = true }))) - - -- Test press on endpoint 9 (button2) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - matter_device, 9, { - new_position = 1, - previous_position = 1, - total_number_of_presses_counted = 1 - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.pushed({ state_change = true }))) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - matter_device, 9, { - new_position = 1, - previous_position = 1, - total_number_of_presses_counted = 2 - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.double({ state_change = true }))) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - matter_device, 9, { - new_position = 1, - previous_position = 0, - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.held({ state_change = true }))) - - -- Test press on endpoint 10 (button3) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - matter_device, 10, { - new_position = 1, - previous_position = 1, - total_number_of_presses_counted = 1 - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.pushed({ state_change = true }))) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - matter_device, 10, { - new_position = 1, - previous_position = 1, - total_number_of_presses_counted = 2 - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.double({ state_change = true }))) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - matter_device, 10, { - new_position = 1, - previous_position = 0, - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.held({ state_change = true }))) - - -- Test press on endpoint 11 (button4) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - matter_device, 11, { - new_position = 1, - previous_position = 1, - total_number_of_presses_counted = 1 - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.pushed({ state_change = true }))) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.MultiPressComplete:build_test_event_report( - matter_device, 11, { - new_position = 1, - previous_position = 1, - total_number_of_presses_counted = 2 - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.double({ state_change = true }))) - test.socket.matter:__queue_receive({ - matter_device.id, - clusters.Switch.events.LongPress:build_test_event_report( - matter_device, 11, { - new_position = 1, - previous_position = 0, - } - ) - }) - test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.held({ state_change = true }))) -end) - -test.register_coroutine_test("Test: Device Type Handler - Handles Button (Type 15) and OnOff (Type 256) Device Types with Child Creation", function() - test.socket.matter:__set_channel_ordering("relaxed") - add_parent_device(parent) - local matter_device = create_matter_device("4-button", parent) - add_matter_device(matter_device, parent, "4-button") - test.wait_for_events() - configure_parent(parent) - test.wait_for_events() - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(9), - data_types.Uint16(8), - data_types.Uint16(6), - })) - }) - - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 6) - }) - - test.wait_for_events() - - -- Receive DeviceTypeList report with device type 15 (button) for endpoint 8 - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { - { - device_type = data_types.Uint32(15), - revision = data_types.Uint16(1) - } - }) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { - { - device_type = data_types.Uint32(15), - revision = data_types.Uint16(1) - } - }) - }) - - -- Receive DeviceTypeList report with device type 256 (OnOff) for endpoint 6 - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 6, data_types.Array { - { - device_type = data_types.Uint32(256), - revision = data_types.Uint16(1) - } - }) - }) - parent:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "light-binary", - parent_device_id = parent.id, - parent_assigned_child_key = "6" - }) - test.wait_for_events() - initiate_info_changed(matter_device, "2-button") - subscribe_switch_events(parent, { 8, 9 }) - test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) - test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) - - test.wait_for_events() - - local child_switch = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition("light-binary.yml"), - device_network_id = string.format("%s:6", parent.id), - parent_device_id = parent.id, - parent_assigned_child_key = "6" - }) - test.wait_for_events() - initiate_info_changed(child_switch, "light-binary") - - test.socket.matter:__expect_send({ - parent.id, - clusters.OnOff.attributes.OnOff:subscribe(parent, nil) - }) -end) - -test.register_coroutine_test("Test: 2G Relay - Profile Changes Between light-binary and 2-button Based On Endpoint Availability", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - add_parent_device(parent) - local relay = create_hager_2g_relay("light-binary", parent) - add_switch_matter_device(relay, parent) - test.wait_for_events() - configure_parent(parent) - test.wait_for_events() - - -- Scenario 1: EP3 + EP4 present → light-binary profile - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(3), - data_types.Uint16(4), - })) - }) - -- - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 3) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) - }) - - relay:expect_metadata_update({ profile = "light-binary" }) - relay:expect_metadata_update({ profile = "light-binary" }) - test.wait_for_events() - -- Both endpoints are device type 256 (OnOff) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 3, data_types.Array { - { - device_type = data_types.Uint32(256), - revision = data_types.Uint16(1) - } - }) - }) - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { - { - device_type = data_types.Uint32(256), - revision = data_types.Uint16(1) - } - }) - }) - - parent:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "light-binary", - parent_device_id = parent.id, - parent_assigned_child_key = "4" - }) - local child_switch = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition("light-binary.yml"), - device_network_id = string.format("%s:4", parent.id), - parent_device_id = parent.id, - parent_assigned_child_key = "4" - }) - - initiate_info_changed(relay, "light-binary") - test.socket.matter:__expect_send({ - parent.id, - clusters.OnOff.attributes.OnOff:subscribe(parent, nil) - }) - - initiate_info_changed(child_switch, "light-binary") - test.socket.matter:__expect_send({ - parent.id, - clusters.OnOff.attributes.OnOff:subscribe(parent, nil) - }) - - --Scenario 2: EP4 removed → profile changes to 2-button, child created for EP3 - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(3), - data_types.Uint16(8), - data_types.Uint16(9), - })) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) - }) - - parent:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "light-binary", - parent_device_id = parent.id, - parent_assigned_child_key = "3" - }) - - relay:expect_metadata_update({ profile = "2-button" }) - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { - { - device_type = data_types.Uint32(15), - revision = data_types.Uint16(1) - } - }) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { - { - device_type = data_types.Uint32(15), - revision = data_types.Uint16(1) - } - }) - }) - - -- Scenario 3: EP4 reappears → profile changes back to light-binary - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(3), - data_types.Uint16(4), - })) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) - }) - relay:expect_metadata_update({ profile = "light-binary" }) - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { - { - device_type = data_types.Uint32(256), - revision = data_types.Uint16(1) - } - }) - }) - - parent:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "light-binary", - parent_device_id = parent.id, - parent_assigned_child_key = "4" - }) - - -- Scenario 4: EP3 removed → profile changes to 2-button, child created for EP4 - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(4), - data_types.Uint16(12), - data_types.Uint16(13), - })) - }) - - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 13) - }) - - relay:expect_metadata_update({ profile = "2-button" }) - - -- Scenario 5: EP3 and EP4 removed → 4-button profile - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(8), - data_types.Uint16(9), - data_types.Uint16(10), - data_types.Uint16(11), - })) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 10) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 11) - }) - relay:expect_metadata_update({ profile = "4-button" }) - - -- Scenario 6: Only EP4 present → 2-button profile, child created - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(4), - data_types.Uint16(8), - data_types.Uint16(9), - })) - }) - - relay:expect_metadata_update({ profile = "2-button" }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) - }) - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { - { - device_type = data_types.Uint32(256), - revision = data_types.Uint16(1) - } - }) - }) - - parent:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "light-binary", - parent_device_id = parent.id, - parent_assigned_child_key = "4" - }) -end) - -test.register_coroutine_test("Test: Dimmer Device - Child Creation for Dimmable Endpoint with Button Support", function() - test.socket.matter:__set_channel_ordering("relaxed") - - add_parent_device(parent) - local dimmer = create_hager_dimmer_device_2g("2-button", parent) - add_matter_device(dimmer, parent, "2-button") - test.wait_for_events() - configure_parent(parent) - test.wait_for_events() - - --Scenario 1: 2 button endpoints (8, 9) detected → 2-button profile - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(8), - data_types.Uint16(9), - data_types.Uint16(3) - })) - }) - - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 3) - }) - dimmer:expect_metadata_update({ profile = "2-button" }) - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 3, data_types.Array { - { - device_type = data_types.Uint32(257), - revision = data_types.Uint16(1) - } - }) - }) - - parent:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "light-level", - parent_device_id = parent.id, - parent_assigned_child_key = "3" - }) - - -- Create mock child device for EP3 - local child_dimmer = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition("light-level.yml"), - device_network_id = string.format("%s:3", parent.id), - parent_device_id = parent.id, - parent_assigned_child_key = "3" - }) - initiate_info_changed(child_dimmer, "light-level") - - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) - }) - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) - }) - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) - }) - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) - }) - - test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) - child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switch", "on") - - test.socket.matter:__expect_send({ - parent.id, - clusters.OnOff.server.commands.On(child_dimmer, 3) - }) - test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 20 } } }) - - child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switchLevel", "setLevel") - - test.socket.matter:__expect_send({ - parent.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(child_dimmer, 3, 51, nil, 0, 0) - }) - -end) - -test.register_coroutine_test("Test: 1G Dimmer - Initialization and Profile Update to light-level", function() - test.socket.matter:__set_channel_ordering("relaxed") - add_parent_device(parent) - local dimmer = create_hager_dimmer_device_2g("2-button", parent) - add_matter_device(dimmer, parent, "2-button") - test.wait_for_events() - configure_parent(parent) -end) - -test.register_coroutine_test("Test: 1G Dimmer - Host Commands and Level Control Capabilities", function() - test.socket.matter:__set_channel_ordering("relaxed") - test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") - - add_parent_device(parent_1g) - local dimmer = create_hager_dimmer_device_1g("light-level", parent_1g) - add_matter_device(dimmer, parent_1g, "light-level") - test.wait_for_events() - configure_parent(parent_1g) - - -- Send dimmable endpoint 4 detection (device type 257) to trigger profile change and subscriptions - test.socket.matter:__queue_receive({ - parent_1g.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_1g, 2, data_types.Array({ - data_types.Uint16(4), - })) - }) - - test.socket.matter:__expect_send({ - parent_1g.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_1g, 4) - }) - - test.wait_for_events() - - test.socket.matter:__queue_receive({ - parent_1g.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_1g, 4, data_types.Array { - { - device_type = data_types.Uint32(257), - revision = data_types.Uint16(1) - } - }) - }) - - ----subscriptions from device_type_handler - - initiate_info_changed(dimmer, "light-level") - test.socket.matter:__expect_send({ - parent_1g.id, - cluster_base.subscribe(parent_1g, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) - }) - test.socket.matter:__expect_send({ - parent_1g.id, - cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) - }) - test.socket.matter:__expect_send({ - parent_1g.id, - cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) - }) - test.socket.matter:__expect_send({ - parent_1g.id, - cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) - }) - - test.socket.capability:__queue_receive({ dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) - dimmer.expect_native_cmd_handler_registration(dimmer, "switch", "on") - - test.socket.matter:__expect_send({ - parent_1g.id, - clusters.OnOff.server.commands.On(dimmer, 4) - }) - test.socket.capability:__queue_receive({ dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 20 } } }) - - dimmer.expect_native_cmd_handler_registration(dimmer, "switchLevel", "setLevel") - - test.socket.matter:__expect_send({ - parent_1g.id, - clusters.LevelControl.server.commands.MoveToLevelWithOnOff(dimmer, 4, 51, nil, 0, 0) - }) -end) - -test.register_coroutine_test("Test: PIR Device - Initialization with Motion and Illuminance Capabilities", function() - test.socket.matter:__set_channel_ordering("relaxed") - add_parent_device(parent_pir) - local pir_device = create_hager_pir_device("motion-illuminance", parent_pir) - add_matter_device(pir_device, parent_pir, "motion-illuminance") - test.wait_for_events() - configure_parent(parent_pir) - -end) - -test.register_coroutine_test("Test: PIR Device - Complete Functionality with Motion, Illuminance, and Dimmer Support", function() - test.socket.matter:__set_channel_ordering("relaxed") - add_parent_device(parent_pir) - local pir_device = create_hager_pir_device("motion-illuminance", parent_pir) - add_matter_device(pir_device, parent_pir, "motion-illuminance") - test.wait_for_events() - - configure_parent(parent_pir) - - test.wait_for_events() - - test.socket.matter:__queue_receive({ - parent_pir.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_pir, 2, data_types.Array({ - data_types.Uint16(3), - data_types.Uint16(4), - data_types.Uint16(5) - - })) - }) - test.socket.matter:__expect_send({ - parent_pir.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 3) - }) - test.socket.matter:__expect_send({ - parent_pir.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 4) - }) - test.socket.matter:__expect_send({ - parent_pir.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 5) - }) - - test.socket.matter:__queue_receive({ - parent_pir.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 4, data_types.Array { - { - device_type = data_types.Uint32(263), - revision = data_types.Uint16(1) - } - }) - }) - test.socket.matter:__queue_receive({ - parent_pir.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 5, data_types.Array { - { - device_type = data_types.Uint32(262), - revision = data_types.Uint16(1) - } - }) - }) - test.socket.matter:__queue_receive({ - parent_pir.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 3, data_types.Array { - { - device_type = data_types.Uint32(257), - revision = data_types.Uint16(1) - } - }) - }) - - parent_pir:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "light-level", - parent_device_id = parent_pir.id, - parent_assigned_child_key = "3" - }) - test.wait_for_events() - initiate_info_changed(pir_device, "motion-illuminance") - test.socket.matter:__expect_send({ - parent_pir.id, - cluster_base.subscribe(parent_pir, nil, clusters.IlluminanceMeasurement.ID, clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID, nil) - }) - test.socket.matter:__expect_send({ - parent_pir.id, - cluster_base.subscribe(parent_pir, nil, clusters.OccupancySensing.ID, clusters.OccupancySensing.attributes.Occupancy.ID, nil) - }) - - local child_dimmer = test.mock_device.build_test_child_device({ - label = "Hager G2 4x Button 1", - profile = t_utils.get_profile_definition("light-level.yml"), - device_network_id = string.format("%s:3", parent_pir.id), - parent_device_id = parent_pir.id, - parent_assigned_child_key = "3" - }) - - test.mock_device.add_test_device(child_dimmer) - initiate_info_changed(child_dimmer, "light-level") - test.socket.matter:__expect_send({ - parent_pir.id, - cluster_base.subscribe(parent_pir, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) - }) - test.socket.matter:__expect_send({ - parent_pir.id, - cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) - }) - test.socket.matter:__expect_send({ - parent_pir.id, - cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) - }) - test.socket.matter:__expect_send({ - parent_pir.id, - cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) - }) - test.wait_for_events() - --Verify motion detected event - test.socket.matter:__queue_receive({ - parent_pir.id, - clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(parent_pir, 6, 1) - }) - - test.socket.capability:__expect_send(pir_device:generate_test_message("main", capabilities.motionSensor.motion.active())) - - -- Verify illuminance measurement event - test.socket.matter:__queue_receive({ - parent_pir.id, - clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_report_data(parent_pir, 5, 21370) - }) - test.socket.capability:__expect_send(pir_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance(137))) - - -- Send on command to OnOff endpoint (dimmer) - child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switch", "on") - - test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) - test.socket.matter:__expect_send({ - parent_pir.id, - clusters.OnOff.commands.On(parent_pir, 3) - }) - -- Verify on state via attribute report - test.socket.matter:__queue_receive({ - parent_pir.id, - clusters.OnOff.attributes.OnOff:build_test_report_data(parent_pir, 3, true) - }) - test.socket.capability:__expect_send(child_dimmer:generate_test_message("main", capabilities.switch.switch.on())) - parent_pir.expect_native_attr_handler_registration(parent_pir, "switch", "switch") - - -- Set dimmer level to 50% - test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 50 } } }) - test.socket.matter:__expect_send({ - parent_pir.id, - clusters.LevelControl.commands.MoveToLevelWithOnOff(parent_pir, 3, 127, nil, 0, 0) - }) - child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switchLevel", "setLevel") - - -- Verify level via attribute report - test.socket.matter:__queue_receive({ - parent_pir.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(parent_pir, 3, 127) - }) - test.socket.capability:__expect_send(child_dimmer:generate_test_message("main", capabilities.switchLevel.level(50))) - parent_pir.expect_native_attr_handler_registration(parent_pir, "switchLevel", "level") -end) - -test.register_coroutine_test("Test: Host with Window Covering - 2-Button Profile with Window Covering Child Device", function() - test.socket.matter:__set_channel_ordering("relaxed") - add_parent_device(parent) - local window_covering_device = create_matter_device_with_window("2-button", parent) - add_matter_device(window_covering_device, parent, "2-button") - test.wait_for_events() - configure_parent(parent) - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(8), - data_types.Uint16(9), - data_types.Uint16(12), - })) - }) - - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) - }) - - -- DeviceTypeList report for window covering endpoint (type 514 = Window Covering Device) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { - { - device_type = data_types.Uint32(514), - revision = data_types.Uint16(1) - } - }) - }) - - parent:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "window-covering", - parent_device_id = parent.id, - parent_assigned_child_key = "12" - }) - - local child_wc = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition("window-covering.yml"), - device_network_id = string.format("%s:12", parent.id), - parent_device_id = parent.id, - parent_assigned_child_key = "12" - }) - - test.mock_device.add_test_device(child_wc) - initiate_info_changed(child_wc, "window-covering") - - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID, nil) - }) - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) - }) - - -- Open command - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "open", args = {} } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.UpOrOpen(parent, 12) }) - test.wait_for_events() - - -- Verify open state via attribute report - test.socket.matter:__queue_receive({ - parent.id, - clusters.WindowCovering.attributes.OperationalStatus:build_test_report_data(parent, 12, 0x01) - }) - test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.opening())) - - - -- Close command - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "close", args = {} } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.DownOrClose(parent, 12) }) - - -- Verify close state via attribute report - test.socket.matter:__queue_receive({ - parent.id, - clusters.WindowCovering.attributes.OperationalStatus:build_test_report_data(parent, 12, 0x02) - }) - test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.closing())) - - -- Pause command - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "pause", args = {} } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.StopMotion(parent, 12) }) - test.wait_for_events() - - -- Set shade level to 50% - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 } } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 5000, nil, 0, 0) }) - test.wait_for_events() - - -- Verify shade level via attribute report - test.socket.matter:__queue_receive({ - parent.id, - clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 5000) - }) - test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50))) - test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.partially_open())) - - - -- Set shade level to 100% - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 100 } } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 0, nil, 0, 0) }) - test.wait_for_events() - - -- Verify shade level via attribute report - test.socket.matter:__queue_receive({ - parent.id, - clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 0) - }) - test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100))) - test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.open())) - - -- Set shade level to 0% - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 0 } } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 10000, nil, 0, 0) }) - - -- Verify shade level via attribute report - test.socket.matter:__queue_receive({ - parent.id, - clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 10000) - }) - test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0))) - test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.closed())) - -end) - -test.register_coroutine_test("Test: Window Covering - Preference Changes for Reverse Polarity and Preset Position", function() - test.socket.matter:__set_channel_ordering("relaxed") - add_parent_device(parent) - local window_covering_device = create_matter_device_with_window("2-button", parent) - add_matter_device(window_covering_device, parent, "2-button") - test.wait_for_events() - configure_parent(parent) - - test.wait_for_events() - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(8), - data_types.Uint16(9), - data_types.Uint16(12), - })) - }) - -- - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) - }) - test.wait_for_events() - - -- DeviceTypeList reports for button endpoints (type 15 = Generic Switch) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { - { - device_type = data_types.Uint32(15), - revision = data_types.Uint16(1) - } - }) - }) - - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { - { - device_type = data_types.Uint32(15), - revision = data_types.Uint16(1) - } - }) - }) - - -- DeviceTypeList report for window covering endpoint (type 514 = Window Covering Device) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { - { - device_type = data_types.Uint32(514), - revision = data_types.Uint16(1) - } - }) - }) - - parent:expect_device_create({ - type = "EDGE_CHILD", - label = "Hager G2 4x Button 1", - profile = "window-covering", - parent_device_id = parent.id, - parent_assigned_child_key = "12" - }) - - local child_wc = test.mock_device.build_test_child_device({ - profile = t_utils.get_profile_definition("window-covering.yml"), - device_network_id = string.format("%s:12", parent.id), - parent_device_id = parent.id, - parent_assigned_child_key = "12" - }) - test.mock_device.add_test_device(child_wc) - - test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { reverse = "false" } })) - test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { reverse = "true" } })) - test.wait_for_events() - local reverse_preference_set = child_wc.preferences.reverse - assert(reverse_preference_set == "true", "reverse_preference_set is True") - -- - --Send open command - with reverse_polarity true, this should send DownOrClose - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "open", args = {} } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.DownOrClose(parent, 12) }) - - -- Send close command - with reverse_polarity true, this should send UpOrOpen - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "close", args = {} } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.UpOrOpen(parent, 12) }) - - -- Send Pause command - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "pause", args = {} } }) - test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.StopMotion(parent, 12) }) - - -- Position preset testing - test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { presetPosition = "50" } })) - test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { presetPosition = "20" } })) - - test.wait_for_events() - - local PRESET_LEVEL_KEY = child_wc.preferences.presetPosition - assert(PRESET_LEVEL_KEY == "20", " presetPosition is set to 20") - - test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } }) - test.socket.matter:__expect_send( - { parent.id, clusters.WindowCovering.server.commands.GoToLiftPercentage(parent, 12, 8000) } - ) - - test.wait_for_events() - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(8), - data_types.Uint16(9), - })) - }) - - test.wait_for_events() - - test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") - test.mock_time.advance_time(5) - local BUTTON_EPS_FIELD = parent:get_field(BUTTON_EPS) - assert(BUTTON_EPS_FIELD[1] == 8) - assert(BUTTON_EPS_FIELD[2] == 9) - - test.wait_for_events() - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ - data_types.Uint16(8), - data_types.Uint16(9), - data_types.Uint16(12), - })) - }) - test.socket.matter:__expect_send({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) - }) - test.socket.matter:__queue_receive({ - parent.id, - clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { - { - device_type = data_types.Uint32(514), - revision = data_types.Uint16(1) - } - }) - }) - child_wc:expect_metadata_update({ profile = "window-covering" }) - initiate_info_changed(child_wc, "window-covering") - - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID, nil) - }) - test.socket.matter:__expect_send({ - parent.id, - cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) - }) -end) - -test.run_registered_tests() From 95a26a6667d9b8bec1b310903994d9554143d16b Mon Sep 17 00:00:00 2001 From: JanJakubiszyn <101173721+JanJakubiszyn@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:15:28 +0200 Subject: [PATCH 20/24] fix: reapply Hager WAASYS device support (#3104) This reverts commit a85de74f33374ab9a5d436ff2ed2493fe6577ca3, reversing changes made to 52ae9953ccb435da4c27535ab58c7313f18fc12b. --- .../matter-switch/fingerprints.yml | 20 + .../profiles/motion-illuminance.yml | 14 + .../profiles/window-covering.yml | 21 + .../src/embedded_clusters/Descriptor/init.lua | 4 + .../server/attributes/DeviceTypeList.lua | 78 + .../Descriptor/types/DeviceTypeStruct.lua | 82 + .../Descriptor/types/init.lua | 17 + .../SmartThings/matter-switch/src/init.lua | 1 + .../src/sub_drivers/hager/can_handle.lua | 13 + .../src/sub_drivers/hager/init.lua | 483 +++++ .../switch_handlers/attribute_handlers.lua | 2 +- .../src/switch_utils/device_configuration.lua | 1 - .../matter-switch/src/switch_utils/fields.lua | 10 + .../src/test/test_hager_waasys.lua | 1780 +++++++++++++++++ 14 files changed, 2524 insertions(+), 2 deletions(-) create mode 100644 drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml create mode 100644 drivers/SmartThings/matter-switch/profiles/window-covering.yml create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/DeviceTypeList.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/types/DeviceTypeStruct.lua create mode 100644 drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/types/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua create mode 100644 drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua create mode 100644 drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 930e38e559..d6a367c685 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -909,6 +909,26 @@ matterManufacturer: vendorId: 0x1285 productId: 0x0009 deviceProfileName: 4-button-batteryLevel + - id: "4741/5" + deviceLabel: Hager Switch 1G + vendorId: 0x1285 + productId: 0x0005 + deviceProfileName: matter-bridge + - id: "4741/6" + deviceLabel: Hager Switch 2G + vendorId: 0x1285 + productId: 0x0006 + deviceProfileName: matter-bridge + - id: "4741/7" + deviceLabel: Hager PIR 1.1M + vendorId: 0x1285 + productId: 0x0007 + deviceProfileName: matter-bridge + - id: "4741/10" + deviceLabel: Hager PIR 2.2M + vendorId: 0x1285 + productId: 0x000A + deviceProfileName: matter-bridge # HAOJAI - id: "5530/4113" deviceLabel: HAOJAI Smart Switch 3-key diff --git a/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml b/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml new file mode 100644 index 0000000000..dfe052043c --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/motion-illuminance.yml @@ -0,0 +1,14 @@ +name: motion-illuminance +components: +- id: main + capabilities: + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: MotionSensor diff --git a/drivers/SmartThings/matter-switch/profiles/window-covering.yml b/drivers/SmartThings/matter-switch/profiles/window-covering.yml new file mode 100644 index 0000000000..b588b41ec9 --- /dev/null +++ b/drivers/SmartThings/matter-switch/profiles/window-covering.yml @@ -0,0 +1,21 @@ +name: window-covering +components: +- id: main + capabilities: + - id: windowShade + version: 1 + - id: windowShadePreset + version: 1 + - id: windowShadeLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Blind +preferences: + - preferenceId: presetPosition + explicit: true + - preferenceId: reverse + explicit: true diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua index fb7a38e63b..12419a9a26 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/init.lua @@ -3,6 +3,7 @@ local cluster_base = require "st.matter.cluster_base" local DescriptorServerAttributes = require "embedded_clusters.Descriptor.server.attributes" +local DescriptorTypes = require "embedded_clusters.Descriptor.types" local Descriptor = {} @@ -11,9 +12,11 @@ Descriptor.NAME = "Descriptor" Descriptor.server = {} Descriptor.client = {} Descriptor.server.attributes = DescriptorServerAttributes:set_parent_cluster(Descriptor) +Descriptor.types = DescriptorTypes function Descriptor:get_attribute_by_id(attr_id) local attr_id_map = { + [0x0000] = "DeviceTypeList", [0x0003] = "PartsList", } local attr_name = attr_id_map[attr_id] @@ -33,6 +36,7 @@ function Descriptor:get_server_command_by_id(command_id) end Descriptor.attribute_direction_map = { + ["DeviceTypeList"] = "server", ["PartsList"] = "server", } diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/DeviceTypeList.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/DeviceTypeList.lua new file mode 100644 index 0000000000..7bd42e15eb --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/server/attributes/DeviceTypeList.lua @@ -0,0 +1,78 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local data_types = require "st.matter.data_types" +local TLVParser = require "st.matter.TLV.TLVParser" + +local DeviceTypeList = { + ID = 0x0000, + NAME = "DeviceTypeList", + base_type = require "st.matter.data_types.Array", + element_type = require "embedded_clusters.Descriptor.types.DeviceTypeStruct", +} + +function DeviceTypeList:augment_type(data_type_obj) + for i, v in ipairs(data_type_obj.elements) do + data_type_obj.elements[i] = data_types.validate_or_build_type(v, DeviceTypeList.element_type) + end +end + +function DeviceTypeList:new_value(...) + local o = self.base_type(table.unpack({ ... })) + return o +end + +function DeviceTypeList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function DeviceTypeList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function DeviceTypeList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function DeviceTypeList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + self:augment_type(data) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function DeviceTypeList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(DeviceTypeList, { __call = DeviceTypeList.new_value, __index = DeviceTypeList.base_type }) +return DeviceTypeList + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/types/DeviceTypeStruct.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/types/DeviceTypeStruct.lua new file mode 100644 index 0000000000..0cb771558d --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/types/DeviceTypeStruct.lua @@ -0,0 +1,82 @@ +-- Copyright 2026 SmartThings +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local StructureABC = require "st.matter.data_types.base_defs.StructureABC" + +local DeviceTypeStruct = {} +local new_mt = StructureABC.new_mt({ NAME = "DeviceTypeStruct", ID = data_types.name_to_id_map["Structure"] }) + +DeviceTypeStruct.field_defs = { + { + name = "device_type", + field_id = 0, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint32", + }, + { + name = "revision", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint16", + }, +} + +DeviceTypeStruct.init = function(cls, tbl) + local o = {} + o.elements = {} + o.num_elements = 0 + setmetatable(o, new_mt) + for _idx, field_def in ipairs(cls.field_defs) do + -- Note: _idx is 1 when field_id is 0 + if (not field_def.is_optional and not field_def.is_nullable) and not tbl[field_def.name] then + error("Missing non optional or non_nullable field: " .. field_def.name) + elseif not (field_def.is_optional and tbl[field_def.name] == nil) then + o.elements[field_def.name] = data_types.validate_or_build_type(tbl[field_def.name], field_def.data_type, field_def.name) + o.elements[field_def.name].field_id = field_def.field_id + o.num_elements = o.num_elements + 1 + end + end + return o +end + +DeviceTypeStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = DeviceTypeStruct.init +new_mt.__index.serialize = DeviceTypeStruct.serialize + +DeviceTypeStruct.augment_type = function(self, val) + local elems = {} + local num_elements = 0 + for _, v in pairs(val.elements) do + for _, field_def in ipairs(self.field_defs) do + if field_def.field_id == v.field_id and + field_def.is_nullable and + (v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, data_types.Null, field_def.field_name) + num_elements = num_elements + 1 + elseif field_def.field_id == v.field_id and not + (field_def.is_optional and v.value == nil and v.elements == nil) then + elems[field_def.name] = data_types.validate_or_build_type(v, field_def.data_type, field_def.field_name) + num_elements = num_elements + 1 + if field_def.element_type ~= nil then + for i, e in ipairs(elems[field_def.name].elements) do + elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) + end + end + end + end + end + val.elements = elems + val.num_elements = num_elements + setmetatable(val, new_mt) +end + +setmetatable(DeviceTypeStruct, new_mt) + +return DeviceTypeStruct + diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/types/init.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/types/init.lua new file mode 100644 index 0000000000..8ecaa987fd --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/Descriptor/types/init.lua @@ -0,0 +1,17 @@ +-- Copyright 2026 SmartThings +-- Licensed under the Apache License, Version 2.0 + +local types_mt = {} +types_mt.__types_cache = {} +types_mt.__index = function(self, key) + if types_mt.__types_cache[key] == nil then + types_mt.__types_cache[key] = require("embedded_clusters.Descriptor.types." .. key) + end + return types_mt.__types_cache[key] +end + +local DescriptorTypes = {} + +setmetatable(DescriptorTypes, types_mt) + +return DescriptorTypes diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 577820c171..9d4e22fb15 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -374,6 +374,7 @@ local matter_driver_template = { switch_utils.lazy_load_if_possible("sub_drivers.aqara_cube"), switch_utils.lazy_load("sub_drivers.camera"), switch_utils.lazy_load_if_possible("sub_drivers.eve_energy"), + switch_utils.lazy_load_if_possible("sub_drivers.hager"), switch_utils.lazy_load_if_possible("sub_drivers.ikea_scroll"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_garage_door"), switch_utils.lazy_load_if_possible("sub_drivers.third_reality_mk1") diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua new file mode 100644 index 0000000000..3ac75a5805 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local device_lib = require "st.device" + local get_product_override_field = require "switch_utils.utils".get_product_override_field + + local checked_device = device.network_type == device_lib.NETWORK_TYPE_MATTER and device or device:get_parent_device() + if get_product_override_field(checked_device, "needs_hager_subdriver") then + return true, require("sub_drivers.hager") + end + return false +end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua new file mode 100644 index 0000000000..db8cfe67d4 --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/hager/init.lua @@ -0,0 +1,483 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local device_lib = require "st.device" +local buttonCfg = require "switch_utils.device_configuration".ButtonCfg +local create_child = require "switch_utils.device_configuration".ChildCfg.create_or_update_child_devices +local switch_utils = require "switch_utils.utils" +local fields = require "switch_utils.fields" +local version = require "version" + +local ACTIVE_EPS = "__active_EPS" +local MATTER_DEVICE_ID = "MATTER_DEVICE_ID" +local PARENT_ID = "PARENT_ID" +local CURRENT_LIFT = "__current_lift" +local BUTTON_EPS = "__button_eps" +local MAIN_WC_EP = "__main_wc_ep" + +local PRODUCT_ID = { + SWITCH_1G = 0x0005, + SWITCH_2G = 0x0006, + PIR_1_1M = 0x0007, + PIR_2_2M = 0x000A, +} + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + +local function subscribe (device, endpoint_id, cluster_id, attr_id, event_id) + device:send(cluster_base.subscribe(device, endpoint_id, cluster_id, attr_id, event_id)) +end + +local function get_parent (driver, device) + return driver:get_device_info(device:get_field(PARENT_ID) or nil) +end + +local function get_matter_device(device) + local matter_device_id = device:get_field(MATTER_DEVICE_ID) + if matter_device_id then + local driver = device.driver + if driver then + local matter_device = driver:get_device_info(matter_device_id) + if matter_device then + return matter_device + end + end + end + return nil +end + +local function extract_reported_endpoints(ib) + local eps = {} + if ib.data and ib.data.elements then + for _, el in ipairs(ib.data.elements) do + local ep_id = el.value + if type(ep_id) == "number" and ep_id ~= 1 and ep_id ~= 2 then + table.insert(eps, ep_id) + end + end + end + return eps +end + +local function detect_endpoint_changes (device, ib_elements) + local stored_eps = device:get_field(ACTIVE_EPS) or {} + ib_elements = ib_elements or {} + + local old_set, new_set = {}, {} + for _, ep_id in ipairs(stored_eps) do + old_set[ep_id] = true + end + for _, ep_id in ipairs(ib_elements) do + new_set[ep_id] = true + end + + local removed_eps, added_eps = {}, {} + + for ep_id in pairs(old_set) do + if not new_set[ep_id] then + table.insert(removed_eps, ep_id) + end + end + for ep_id in pairs(new_set) do + if not old_set[ep_id] then + table.insert(added_eps, ep_id) + end + end + return removed_eps, added_eps +end + +local function assign_profile_for_endpoint(device_type_id) + local profile = fields.device_type_profile_map[device_type_id] or "switch-binary" + return function(device, ep_id, is_child_device) + return profile, nil + end +end + +local function link_matter_device_and_parent(matter_device) + local parent = matter_device:get_parent_device() + matter_device:set_field(PARENT_ID, parent.id, { persist = true }) + matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(PARENT_ID, parent.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) +end + +local function handle_set_preset(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local lift_value = device.preferences.presetPosition + local hundredths_lift_percent = (100 - tonumber(lift_value)) * 100 + device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage(device, endpoint_id, hundredths_lift_percent)) +end + +local function handle_close(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) + if device.preferences.reverse then + req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) + end + device:send(req) +end + +local function handle_open(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) + if device.preferences.reverse then + req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) + end + device:send(req) +end + +local function handle_pause(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local req = clusters.WindowCovering.server.commands.StopMotion(device, endpoint_id) + device:send(req) +end + +local function handle_shade_level(driver, device, cmd) + local endpoint_id = device:component_to_endpoint(cmd.component) + local lift_percentage_value = 100 - cmd.args.shadeLevel + local hundredths_lift_percentage = lift_percentage_value * 100 + local req = clusters.WindowCovering.server.commands.GoToLiftPercentage( + device, endpoint_id, hundredths_lift_percentage + ) + device:send(req) +end + +local current_pos_handler = function(attribute) + return function(driver, device, ib, response) + if ib.data.value == nil then + return + end + local windowShade = capabilities.windowShade.windowShade + local position = 100 - math.floor(ib.data.value / 100) + local reverse = device.preferences.reverse + device:emit_event_for_endpoint(ib.endpoint_id, attribute(position)) + if attribute == capabilities.windowShadeLevel.shadeLevel then + device:set_field(CURRENT_LIFT, position) + end + local lift_position = device:get_field(CURRENT_LIFT) + if lift_position == 100 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closed() or windowShade.open()) + elseif lift_position > 0 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) + elseif lift_position == 0 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) + end + end +end + +local function current_status_handler(driver, device, ib, response) + local windowShade = capabilities.windowShade.windowShade + local reverse = device.preferences.reverse + local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL + if state == 1 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closing() or windowShade.opening()) + elseif state == 2 then + device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.opening() or windowShade.closing()) + elseif state ~= 0 then + device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) + end +end + +local function info_changed(driver, device, event, args) + if not device or not device.id or not device.profile then + return + end + + if device.profile.id ~= args.old_st_store.profile.id or device.network_type == device_lib.NETWORK_TYPE_CHILD then + local parent = device:get_parent_device() + local matter_device = get_matter_device(parent) + local map = {} + device.thread:call_with_delay(2, function() + if device:supports_capability(capabilities.button) then + local button_eps = parent:get_field(BUTTON_EPS) + local clean_eps = {} + table.sort(button_eps) + for _, v in ipairs(button_eps or {}) do + table.insert(clean_eps, v) + end + + buttonCfg.update_button_component_map(matter_device, clean_eps[1], clean_eps) + for _, ep_id in ipairs(clean_eps) do + subscribe(device, ep_id, clusters.Switch.ID, nil, clusters.Switch.events.MultiPressComplete.ID) + subscribe(device, ep_id, clusters.Switch.ID, nil, clusters.Switch.events.LongPress.ID) + device:emit_event_for_endpoint(ep_id, capabilities.button.supportedButtonValues({ "pushed", "double", "held" })) + end + return + elseif device:supports_capability(capabilities.switch) then + map = { main = 3 } + subscribe(device, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) + if device:supports_capability(capabilities.switchLevel) then + map = { main = 4 } + subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) + subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID) + subscribe(device, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID) + end + elseif device:supports_capability(capabilities.motionSensor) then + subscribe(device, nil, clusters.OccupancySensing.ID, clusters.OccupancySensing.attributes.Occupancy.ID, nil) + subscribe(device, nil, clusters.IlluminanceMeasurement.ID, clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID, nil) + elseif device:supports_capability(capabilities.windowShadeLevel) then + map = { main = 5 } + subscribe(device, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID) + subscribe(device, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) + end + if device.id == matter_device.id then + parent:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, map, { persist = true }) + matter_device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, map, { persist = true }) + end + end) + end +end + +local function device_init (driver, device) + if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then + device.thread:call_with_delay(4, function() + info_changed(driver, device, nil, { + old_st_store = { profile = { } } + }) + end) + return + end + + if device:get_parent_device() ~= nil then + link_matter_device_and_parent(device) + local parent = device:get_parent_device() + device:extend_device("send", function(self, message) + return parent:send(message) + end) + else + subscribe(device, 2, clusters.Descriptor.ID, clusters.Descriptor.attributes.PartsList.ID) + device.thread:call_with_delay(3, function() + local matter_device = get_matter_device(device) + device:extend_device("emit_event_for_endpoint", function(self, ep_info, event) + local endpoint_id = type(ep_info) == "number" and ep_info or ep_info.endpoint_id + local child = self:get_child_by_parent_assigned_key(string.format("%d", endpoint_id)) + if child then + return child:emit_event(event) + end + if matter_device then + return matter_device:emit_event_for_endpoint(ep_info, event) + end + end) + end) + end + + device:set_component_to_endpoint_fn(switch_utils.component_to_endpoint) + device:set_endpoint_to_component_fn(switch_utils.endpoint_to_component) +end + +local function handle_descriptor_report(driver, device, ib, response) + local product_id = device.manufacturer_info and device.manufacturer_info.product_id + local parent = get_parent(driver, device) + + if not parent or ib.endpoint_id ~= 2 then + return + end + + local matter_device = get_matter_device(device) + + local new_eps = extract_reported_endpoints(ib) or {} + table.sort(new_eps) + local removed_eps, added_eps = detect_endpoint_changes(device, new_eps) + + device:set_field(ACTIVE_EPS, new_eps, { persist = true }) + + for _, ep_id in ipairs(added_eps or {}) do + subscribe(parent, ep_id, clusters.Descriptor.ID, clusters.Descriptor.attributes.DeviceTypeList.ID, nil) + + if product_id == PRODUCT_ID.SWITCH_1G and ep_id == 3 then + matter_device:try_update_metadata({ profile = "light-binary" }) + elseif product_id == PRODUCT_ID.SWITCH_2G then + if ep_id == 3 then + local profile = switch_utils.tbl_contains(new_eps, 4) and "light-binary" or "2-button" + matter_device:try_update_metadata({ profile = profile }) + elseif ep_id == 4 then + local profile = switch_utils.tbl_contains(new_eps, 3) and "light-binary" or "2-button" + matter_device:try_update_metadata({ profile = profile }) + end + end + end + + for _, ep_id in ipairs(removed_eps) do + local button_eps = parent:get_field(BUTTON_EPS) or {} + local clean_eps = {} + + for _, value in ipairs(button_eps) do + if value ~= ep_id then + table.insert(clean_eps, value) + end + end + table.sort(clean_eps) + parent:set_field(BUTTON_EPS, clean_eps, { persist = true }) + matter_device:set_field(BUTTON_EPS, clean_eps, { persist = true }) + + local child = parent:get_child_by_parent_assigned_key(tostring(ep_id)) + if child then + driver:try_delete_device(child.id) + end + + if ep_id == 3 then + if device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then + matter_device:try_update_metadata({ profile = "2-button" }) + elseif device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then + local has_ep4 = switch_utils.tbl_contains(new_eps, 4) + matter_device:try_update_metadata({ profile = has_ep4 and "2-button" or "4-button" }) + end + elseif ep_id == 4 then + if device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then + local button_comb = switch_utils.tbl_contains(new_eps, 3) + if button_comb then + matter_device:try_update_metadata({ profile = "2-button" }) + create_child(driver, parent, { 3 }, 1, assign_profile_for_endpoint(fields.DEVICE_TYPE_ID.LIGHT.ON_OFF)) + else + matter_device:try_update_metadata({ profile = "4-button" }) + end + end + end + end +end + +local function device_type_handler (driver, device, ib) + if version.api < 16 then + clusters.Descriptor.server.attributes.DeviceTypeList:augment_type(ib.data) + end + + local parent = get_parent(driver, device) + if not parent then + return + end + + local matter_device = get_matter_device(device) + local stored_btn_eps = parent:get_field(BUTTON_EPS) or {} + local new_btn_eps = {} + local ep_id = ib.endpoint_id + local value = ib.data.elements + + for _, v in ipairs(stored_btn_eps) do + table.insert(new_btn_eps, v) + end + for _, element in ipairs(value) do + local device_type_field = element.elements.device_type + local device_type_id = device_type_field and device_type_field.value + + if device_type_id == fields.DEVICE_TYPE_ID.GENERIC_SWITCH then + if not switch_utils.tbl_contains(new_btn_eps, ep_id) then + switch_utils.set_field_for_endpoint(parent, fields.SUPPORTS_MULTI_PRESS, ep_id) + switch_utils.set_field_for_endpoint(parent, fields.IGNORE_NEXT_MPC, ep_id) + table.insert(new_btn_eps, ep_id) + table.sort(new_btn_eps) + parent:set_field(BUTTON_EPS, new_btn_eps, { persist = true }) + end + end + + if device_type_id == fields.DEVICE_TYPE_ID.LIGHT.ON_OFF then + local active_eps = device:get_field(ACTIVE_EPS) + + if ep_id == 3 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then + return + elseif ep_id == 4 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then + if switch_utils.tbl_contains(active_eps, 3) then + local ep3 = parent:get_child_by_parent_assigned_key("3") + if ep3 then + driver:try_delete_device(ep3.id) + end + else + create_child(driver, parent, { 4 }, 1, assign_profile_for_endpoint(device_type_id)) + return + end + elseif ep_id == 3 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_2G then + if not switch_utils.tbl_contains(active_eps, 4) then + create_child(driver, parent, { 3 }, 1, assign_profile_for_endpoint(device_type_id)) + else + driver:try_delete_device(ep_id) + end + return + end + create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) + elseif device_type_id == fields.DEVICE_TYPE_ID.LIGHT.DIMMABLE then + if ep_id == 4 and device.manufacturer_info.product_id == PRODUCT_ID.SWITCH_1G then + return + end + create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) + elseif device_type_id == fields.DEVICE_TYPE_ID.WINDOW_COVERING then + if matter_device:get_field(MAIN_WC_EP) == nil then + create_child(driver, parent, { ep_id }, 1, assign_profile_for_endpoint(device_type_id)) + end + end + end +end + +local function do_configure (driver, device) + if device.network_type == device_lib.NETWORK_TYPE_MATTER then + + local wc_eps = device:get_endpoints(clusters.WindowCovering.ID) + local oc_eps = device:get_endpoints(clusters.OccupancySensing.ID) + local bt_eps = device:get_endpoints(clusters.Switch.ID) + local lvl_eps = device:get_endpoints(clusters.LevelControl.ID) + local product_id = device.manufacturer_info.product_id + + table.sort(wc_eps) + table.sort(oc_eps) + table.sort(lvl_eps) + + if #oc_eps > 0 then + device:try_update_metadata({ profile = "motion-illuminance" }) + elseif #wc_eps > 0 and product_id == PRODUCT_ID.SWITCH_1G then + device:try_update_metadata({ profile = "window-covering" }) + device:set_field(MAIN_WC_EP, wc_eps[1], { persist = true }) + elseif #bt_eps == 4 then + device:try_update_metadata({ profile = "4-button" }) + elseif #bt_eps == 2 then + device:try_update_metadata({ profile = "2-button" }) + elseif #lvl_eps > 0 and product_id == PRODUCT_ID.SWITCH_1G then + device:try_update_metadata({ profile = "light-level" }) + end + device:set_field(BUTTON_EPS, bt_eps, { persist = true }) + end +end + +local function added (driver, device) end +local function driver_switched(driver, device) end + +local hager_switch = { + NAME = "Hager Subdriver", + lifecycle_handlers = { + added = added, + init = device_init, + infoChanged = info_changed, + doConfigure = do_configure, + driverSwitched = driver_switched + }, + matter_handlers = { + attr = { + [clusters.Descriptor.ID] = { + [clusters.Descriptor.attributes.PartsList.ID] = handle_descriptor_report, + [clusters.Descriptor.attributes.DeviceTypeList.ID] = device_type_handler, + }, + [clusters.WindowCovering.ID] = { + [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_pos_handler(capabilities.windowShadeLevel.shadeLevel), + [clusters.WindowCovering.attributes.OperationalStatus.ID] = current_status_handler, + }, + }, + }, + capability_handlers = { + [capabilities.windowShadePreset.ID] = { + [capabilities.windowShadePreset.commands.presetPosition.NAME] = handle_set_preset, + }, + [capabilities.windowShade.ID] = { + [capabilities.windowShade.commands.close.NAME] = handle_close, + [capabilities.windowShade.commands.open.NAME] = handle_open, + [capabilities.windowShade.commands.pause.NAME] = handle_pause, + }, + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = handle_shade_level, + }, + }, + can_handle = require("sub_drivers.hager.can_handle") +} + +return hager_switch diff --git a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua index 45694bc613..b7feed6344 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -246,7 +246,7 @@ end -- [[ OCCUPANCY CLUSTER ATTRIBUTES ]] -- function AttributeHandlers.occupancy_handler(driver, device, ib, response) - device:emit_event(ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) + device:emit_event_for_endpoint(ib.endpoint_id, ib.data.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) 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 772669ebd7..9c5235d1e2 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -156,7 +156,6 @@ function ButtonDeviceConfiguration.update_button_component_map(device, default_e device:set_field(fields.COMPONENT_TO_ENDPOINT_MAP, component_map, {persist = true}) end - function ButtonDeviceConfiguration.configure_buttons(device, momentary_switch_ep_ids) local msr_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_RELEASE}) local msl_eps = device:get_endpoints(clusters.Switch.ID, {feature_bitmap=clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH_LONG_PRESS}) diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua index a8e9781172..5873cb40b2 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/fields.lua @@ -34,7 +34,9 @@ SwitchFields.DEVICE_TYPE_ID = { ELECTRICAL_SENSOR = 0x0510, FAN = 0x002B, GENERIC_SWITCH = 0x000F, + LIGHT_SENSOR = 0x0106, IRRIGATION_SYSTEM = 0x0040, + OCCUPANCY_SENSOR = 0x0107, MOUNTED_ON_OFF_CONTROL = 0x010F, MOUNTED_DIMMABLE_LOAD_CONTROL = 0x0110, ON_OFF_PLUG_IN_UNIT = 0x010A, @@ -50,6 +52,7 @@ SwitchFields.DEVICE_TYPE_ID = { COLOR_DIMMER = 0x0105, }, WATER_VALVE = 0x0042, + WINDOW_COVERING = 0x0202 } SwitchFields.device_type_profile_map = { @@ -64,6 +67,7 @@ SwitchFields.device_type_profile_map = { [SwitchFields.DEVICE_TYPE_ID.DIMMABLE_PLUG_IN_UNIT] = "plug-level", [SwitchFields.DEVICE_TYPE_ID.MOUNTED_ON_OFF_CONTROL] = "switch-binary", [SwitchFields.DEVICE_TYPE_ID.MOUNTED_DIMMABLE_LOAD_CONTROL] = "switch-level", + [SwitchFields.DEVICE_TYPE_ID.WINDOW_COVERING] = "window-covering" } -- COMPONENT_TO_ENDPOINT_MAP is here to preserve the endpoint mapping for @@ -116,6 +120,12 @@ SwitchFields.vendor_overrides = { [0x1189] = { -- LEDVANCE_MANUFACTURER_ID [0x0891] = { target_profile = "switch-binary", initial_profile = "light-binary" }, }, + [0x1285] = { -- HAGER_MANUFACTURER_ID + [0x0005] = { needs_hager_subdriver = true }, -- Hager HBnet 1g switch + [0x0006] = { needs_hager_subdriver = true }, -- Hager HBnet 2g switch + [0x0007] = { needs_hager_subdriver = true }, -- Hager HBnet PIR 1.1M + [0x000A] = { needs_hager_subdriver = true }, -- Hager HBnet PIR 2.2M + }, [0x1321] = { -- SONOFF_MANUFACTURER_ID [0x000C] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, [0x000D] = { target_profile = "switch-binary", initial_profile = "plug-binary" }, diff --git a/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua new file mode 100644 index 0000000000..96a684529d --- /dev/null +++ b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua @@ -0,0 +1,1780 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local data_types = require "st.matter.data_types" +local clusters = require "st.matter.clusters" +local cluster_base = require "st.matter.cluster_base" +local descriptor = require "embedded_clusters.Descriptor" +local version = require "version" + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + +local MATTER_DEVICE_ID = "MATTER_DEVICE_ID" +local PARENT_ID = "PARENT_ID" +local BUTTON_EPS = "__button_eps" + +local function create_parent_device(product_id) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 4x Button", + profile = t_utils.get_profile_definition("matter-bridge.yml"), + manufacturer_info = { + vendor_id = 0x1285, + product_id = product_id, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 1, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } + }, + device_types = { { device_type_id = 0x000E, device_type_revision = 1 } } -- AggregateNode + }, + { + endpoint_id = 2, + clusters = { + { + cluster_id = clusters.Descriptor.ID, + cluster_type = "SERVER", + cluster_revision = 1, + } + }, + device_types = { { device_type_id = 0x0039, device_type_revision = 1 } } -- BridgedNode + } + }, + }) +end + +local function create_matter_device(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 4x Button", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0006, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 8, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 9, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 10, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 11, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + }, + }) +end + +-- Create Hager 2G Relay device with endpoints 3 & 4 (OnOff clusters) +local function create_hager_2g_relay(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 2G Relay", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0006, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + } + }, + device_types = { { device_type_id = 0x0100, device_type_revision = 1 } } + }, + { + endpoint_id = 4, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + } + }, + device_types = { { device_type_id = 0x0100, device_type_revision = 1 } } + }, + } + }) +end + +-- Create Hager Dimmer device with ONLY dimmable endpoint (3) - no button endpoints +local function create_hager_dimmer_device_1g(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 Dimmer", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0005, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 4, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + }, + { + cluster_id = clusters.LevelControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 + } + } + }, + device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } + }, + } + }) +end + +local function create_hager_dimmer_device_2g(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 Dimmer", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0006, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + }, + { + cluster_id = clusters.LevelControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 + } + } + }, + device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } + }, + { + endpoint_id = 8, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 9, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + } + }) +end + +local function create_matter_device_with_window(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 matter device with Window Covering", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0006, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 8, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 9, + clusters = { + { + cluster_id = clusters.Switch.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.Switch.types.SwitchFeature.MOMENTARY_SWITCH, + attributes = { + [clusters.Switch.attributes.MultiPressMax.ID] = 2 + } + } + }, + device_types = { { device_type_id = 0x003B, device_type_revision = 1 } } + }, + { + endpoint_id = 12, + clusters = { + { + cluster_id = clusters.WindowCovering.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = clusters.WindowCovering.types.Feature.LIFT | + clusters.WindowCovering.types.Feature.POSITION_AWARE_LIFT | + clusters.WindowCovering.types.Feature.ABSOLUTE_POSITION, + attributes = { + [clusters.WindowCovering.attributes.OperationalStatus.ID] = 0x00, + [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = 0x0000, + } + } + }, + device_types = { { device_type_id = 0x0202, device_type_revision = 1 } } + }, + } + }) +end + +local function create_hager_pir_device(profile_name, parent) + return test.mock_device.build_test_matter_device({ + label = "Hager G2 PIR with Buttons and Motion/Illuminance/Dimmer", + profile = t_utils.get_profile_definition(profile_name .. ".yml"), + type = "MATTER", + manufacturer_info = { + vendor_id = 0x1285, + product_id = 0x0007, + }, + parent_device_id = parent.id, + endpoints = { + { + endpoint_id = 0, + clusters = { { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } }, + device_types = { { device_type_id = 0x0016, device_type_revision = 1 } } + }, + { + endpoint_id = 3, + clusters = { + { + cluster_id = clusters.OnOff.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OnOff.attributes.OnOff.ID] = false + } + }, + { + cluster_id = clusters.LevelControl.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.LevelControl.attributes.CurrentLevel.ID] = 254 + } + } + }, + device_types = { { device_type_id = 0x0101, device_type_revision = 1 } } + }, + { + endpoint_id = 6, + clusters = { + { + cluster_id = clusters.OccupancySensing.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.OccupancySensing.attributes.Occupancy.ID] = 0 + } + } + }, + device_types = { { device_type_id = 0x0107, device_type_revision = 1 } } + }, + { + endpoint_id = 5, + clusters = { + { + cluster_id = clusters.IlluminanceMeasurement.ID, + cluster_type = "SERVER", + cluster_revision = 1, + attributes = { + [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = 0 + } + } + }, + device_types = { { device_type_id = 0x0106, device_type_revision = 1 } } + }, + } + }) +end + +local function add_parent_device(parent) + test.mock_device.add_test_device(parent) + test.socket.device_lifecycle:__queue_receive({ parent.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ parent.id, "init" }) + test.mock_time.advance_time(5) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, 2, descriptor.ID, descriptor.attributes.PartsList.ID, nil) + }) +end + +local function subscribe_switch_events(parent, button_eps) + for _, ep in ipairs(button_eps) do + test.socket.matter:__expect_send({ + parent.id, + clusters.Switch.events.MultiPressComplete:subscribe(parent, ep) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Switch.events.LongPress:subscribe(parent, ep) + }) + end +end + +local function add_matter_device(matter_device, parent, expected_profile_change) + test.mock_device.add_test_device(matter_device) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "doConfigure" }) + matter_device:expect_metadata_update({ profile = expected_profile_change }) + matter_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "init" }) + + matter_device:set_field(PARENT_ID, parent.id, { persist = true }) + matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(PARENT_ID, parent.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) +end + +local function add_switch_matter_device(matter_device, parent) + test.mock_device.add_test_device(matter_device) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "doConfigure" }) + matter_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ matter_device.id, "init" }) + + matter_device:set_field(PARENT_ID, parent.id, { persist = true }) + matter_device:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(PARENT_ID, parent.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) + parent:set_field(MATTER_DEVICE_ID, matter_device.id, { persist = true }) +end + +local function button_supported_values (matter_device) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) +end + +local function initiate_info_changed(device, profile) + test.socket.device_lifecycle:__queue_receive(device:generate_info_changed({ profile = { id = profile } })) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.mock_time.advance_time(2) +end + +local function configure_parent(device) + test.socket.device_lifecycle:__queue_receive({ device.id, "doConfigure" }) + device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.socket.device_lifecycle:__queue_receive({ device.id, "init" }) + test.socket.matter:__expect_send({ + device.id, + cluster_base.subscribe(device, 2, descriptor.ID, descriptor.attributes.PartsList.ID, nil) + }) + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.mock_time.advance_time(3) +end + +local parent = create_parent_device(0x0006) -- 2G button product +local parent_1g = create_parent_device(0x0005) -- 1G button product +local parent_pir = create_parent_device(0x0007) -- PIR product + +local function test_init() + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test("Test: 4-Button Device Detection - Profile Changes from matter-bridge to 4-button When Four Button Endpoints Present", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local matter_device = create_matter_device("matter-bridge", parent) + add_matter_device(matter_device, parent, "4-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(10), + data_types.Uint16(11), + })) + }) + + local matter_device_parent_id = matter_device:get_field(PARENT_ID) + local matter_device_id = matter_device:get_field(MATTER_DEVICE_ID) + local parent_id = parent:get_field(PARENT_ID) + local parent_matter_device_id = parent:get_field(MATTER_DEVICE_ID) + + assert(matter_device_parent_id == parent.id, "link_host_and_subhub 1/4") + assert(matter_device_id == matter_device.id, "link_host_and_subhub 2/4") + assert(parent_id == parent.id, "link_host_and_subhub 3/4") + assert(parent_matter_device_id == matter_device.id, "link_host_and_subhub 4/4") + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x08) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x09) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0A) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0B) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 10, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 11, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + +end) + +test.register_coroutine_test("Test: Button Event Handling - Pushed, Double Press, and Held Events on 4-Button Device", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local matter_device = create_matter_device("4-button", parent) + add_matter_device(matter_device, parent, "4-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(10), + data_types.Uint16(11), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x08) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x09) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0A) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 0x0B) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 10, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 11, data_types.Array({ + { device_type = 0x000F, revision = 0x0003 } + })) + }) + test.wait_for_events() + + initiate_info_changed(matter_device, "4-button") + subscribe_switch_events(parent, { 8, 9, 10, 11 }) + button_supported_values(matter_device) + test.wait_for_events() + + --Test single press (pushed) on endpoint 8 (button1) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 8, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 1 -- Single press + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true }))) + + --Test double press on endpoint 8 (button1) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 8, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 2 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.double({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + matter_device, 8, { + new_position = 1, + previous_position = 0, + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.button.held({ state_change = true }))) + + -- Test press on endpoint 9 (button2) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 9, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 1 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.pushed({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 9, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 2 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.double({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + matter_device, 9, { + new_position = 1, + previous_position = 0, + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.button.held({ state_change = true }))) + + -- Test press on endpoint 10 (button3) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 10, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 1 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.pushed({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 10, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 2 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.double({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + matter_device, 10, { + new_position = 1, + previous_position = 0, + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button3", capabilities.button.button.held({ state_change = true }))) + + -- Test press on endpoint 11 (button4) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 11, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 1 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.pushed({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + matter_device, 11, { + new_position = 1, + previous_position = 1, + total_number_of_presses_counted = 2 + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.double({ state_change = true }))) + test.socket.matter:__queue_receive({ + matter_device.id, + clusters.Switch.events.LongPress:build_test_event_report( + matter_device, 11, { + new_position = 1, + previous_position = 0, + } + ) + }) + test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.held({ state_change = true }))) +end) + +test.register_coroutine_test("Test: Device Type Handler - Handles Button (Type 15) and OnOff (Type 256) Device Types with Child Creation", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local matter_device = create_matter_device("4-button", parent) + add_matter_device(matter_device, parent, "4-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(9), + data_types.Uint16(8), + data_types.Uint16(6), + })) + }) + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 6) + }) + + test.wait_for_events() + + -- Receive DeviceTypeList report with device type 15 (button) for endpoint 8 + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + + -- Receive DeviceTypeList report with device type 256 (OnOff) for endpoint 6 + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 6, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "6" + }) + test.wait_for_events() + initiate_info_changed(matter_device, "2-button") + subscribe_switch_events(parent, { 8, 9 }) + test.socket.capability:__expect_send(matter_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + test.socket.capability:__expect_send(matter_device:generate_test_message("button2", capabilities.button.supportedButtonValues({ "pushed", "double", "held" }))) + + test.wait_for_events() + + local child_switch = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("light-binary.yml"), + device_network_id = string.format("%s:6", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "6" + }) + test.wait_for_events() + initiate_info_changed(child_switch, "light-binary") + + test.socket.matter:__expect_send({ + parent.id, + clusters.OnOff.attributes.OnOff:subscribe(parent, nil) + }) +end) + +test.register_coroutine_test("Test: 2G Relay - Profile Changes Between light-binary and 2-button Based On Endpoint Availability", function() + test.socket.matter:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + add_parent_device(parent) + local relay = create_hager_2g_relay("light-binary", parent) + add_switch_matter_device(relay, parent) + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + -- Scenario 1: EP3 + EP4 present → light-binary profile + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(3), + data_types.Uint16(4), + })) + }) + -- + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 3) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) + }) + + relay:expect_metadata_update({ profile = "light-binary" }) + relay:expect_metadata_update({ profile = "light-binary" }) + test.wait_for_events() + -- Both endpoints are device type 256 (OnOff) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 3, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "4" + }) + local child_switch = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("light-binary.yml"), + device_network_id = string.format("%s:4", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "4" + }) + + initiate_info_changed(relay, "light-binary") + test.socket.matter:__expect_send({ + parent.id, + clusters.OnOff.attributes.OnOff:subscribe(parent, nil) + }) + + initiate_info_changed(child_switch, "light-binary") + test.socket.matter:__expect_send({ + parent.id, + clusters.OnOff.attributes.OnOff:subscribe(parent, nil) + }) + + --Scenario 2: EP4 removed → profile changes to 2-button, child created for EP3 + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(3), + data_types.Uint16(8), + data_types.Uint16(9), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "3" + }) + + relay:expect_metadata_update({ profile = "2-button" }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + + -- Scenario 3: EP4 reappears → profile changes back to light-binary + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(3), + data_types.Uint16(4), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) + }) + relay:expect_metadata_update({ profile = "light-binary" }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "4" + }) + + -- Scenario 4: EP3 removed → profile changes to 2-button, child created for EP4 + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(4), + data_types.Uint16(12), + data_types.Uint16(13), + })) + }) + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 13) + }) + + relay:expect_metadata_update({ profile = "2-button" }) + + -- Scenario 5: EP3 and EP4 removed → 4-button profile + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(10), + data_types.Uint16(11), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 10) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 11) + }) + relay:expect_metadata_update({ profile = "4-button" }) + + -- Scenario 6: Only EP4 present → 2-button profile, child created + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(4), + data_types.Uint16(8), + data_types.Uint16(9), + })) + }) + + relay:expect_metadata_update({ profile = "2-button" }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 4) + }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 4, data_types.Array { + { + device_type = data_types.Uint32(256), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-binary", + parent_device_id = parent.id, + parent_assigned_child_key = "4" + }) +end) + +test.register_coroutine_test("Test: Dimmer Device - Child Creation for Dimmable Endpoint with Button Support", function() + test.socket.matter:__set_channel_ordering("relaxed") + + add_parent_device(parent) + local dimmer = create_hager_dimmer_device_2g("2-button", parent) + add_matter_device(dimmer, parent, "2-button") + test.wait_for_events() + configure_parent(parent) + test.wait_for_events() + + --Scenario 1: 2 button endpoints (8, 9) detected → 2-button profile + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(3) + })) + }) + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 3) + }) + dimmer:expect_metadata_update({ profile = "2-button" }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 3, data_types.Array { + { + device_type = data_types.Uint32(257), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-level", + parent_device_id = parent.id, + parent_assigned_child_key = "3" + }) + + -- Create mock child device for EP3 + local child_dimmer = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("light-level.yml"), + device_network_id = string.format("%s:3", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "3" + }) + initiate_info_changed(child_dimmer, "light-level") + + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) + }) + + test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) + child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switch", "on") + + test.socket.matter:__expect_send({ + parent.id, + clusters.OnOff.server.commands.On(child_dimmer, 3) + }) + test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 20 } } }) + + child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switchLevel", "setLevel") + + test.socket.matter:__expect_send({ + parent.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(child_dimmer, 3, 51, nil, 0, 0) + }) + +end) + +test.register_coroutine_test("Test: 1G Dimmer - Initialization and Profile Update to light-level", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local dimmer = create_hager_dimmer_device_2g("2-button", parent) + add_matter_device(dimmer, parent, "2-button") + test.wait_for_events() + configure_parent(parent) +end) + +test.register_coroutine_test("Test: 1G Dimmer - Host Commands and Level Control Capabilities", function() + test.socket.matter:__set_channel_ordering("relaxed") + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + + add_parent_device(parent_1g) + local dimmer = create_hager_dimmer_device_1g("light-level", parent_1g) + add_matter_device(dimmer, parent_1g, "light-level") + test.wait_for_events() + configure_parent(parent_1g) + + -- Send dimmable endpoint 4 detection (device type 257) to trigger profile change and subscriptions + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_1g, 2, data_types.Array({ + data_types.Uint16(4), + })) + }) + + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_1g, 4) + }) + + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent_1g.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_1g, 4, data_types.Array { + { + device_type = data_types.Uint32(257), + revision = data_types.Uint16(1) + } + }) + }) + + ----subscriptions from device_type_handler + + initiate_info_changed(dimmer, "light-level") + test.socket.matter:__expect_send({ + parent_1g.id, + cluster_base.subscribe(parent_1g, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_1g.id, + cluster_base.subscribe(parent_1g, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) + }) + + test.socket.capability:__queue_receive({ dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) + dimmer.expect_native_cmd_handler_registration(dimmer, "switch", "on") + + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.OnOff.server.commands.On(dimmer, 4) + }) + test.socket.capability:__queue_receive({ dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 20 } } }) + + dimmer.expect_native_cmd_handler_registration(dimmer, "switchLevel", "setLevel") + + test.socket.matter:__expect_send({ + parent_1g.id, + clusters.LevelControl.server.commands.MoveToLevelWithOnOff(dimmer, 4, 51, nil, 0, 0) + }) +end) + +test.register_coroutine_test("Test: PIR Device - Initialization with Motion and Illuminance Capabilities", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent_pir) + local pir_device = create_hager_pir_device("motion-illuminance", parent_pir) + add_matter_device(pir_device, parent_pir, "motion-illuminance") + test.wait_for_events() + configure_parent(parent_pir) + +end) + +test.register_coroutine_test("Test: PIR Device - Complete Functionality with Motion, Illuminance, and Dimmer Support", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent_pir) + local pir_device = create_hager_pir_device("motion-illuminance", parent_pir) + add_matter_device(pir_device, parent_pir, "motion-illuminance") + test.wait_for_events() + + configure_parent(parent_pir) + + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent_pir, 2, data_types.Array({ + data_types.Uint16(3), + data_types.Uint16(4), + data_types.Uint16(5) + + })) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 3) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 4) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent_pir, 5) + }) + + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 4, data_types.Array { + { + device_type = data_types.Uint32(263), + revision = data_types.Uint16(1) + } + }) + }) + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 5, data_types.Array { + { + device_type = data_types.Uint32(262), + revision = data_types.Uint16(1) + } + }) + }) + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent_pir, 3, data_types.Array { + { + device_type = data_types.Uint32(257), + revision = data_types.Uint16(1) + } + }) + }) + + parent_pir:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "light-level", + parent_device_id = parent_pir.id, + parent_assigned_child_key = "3" + }) + test.wait_for_events() + initiate_info_changed(pir_device, "motion-illuminance") + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.IlluminanceMeasurement.ID, clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.OccupancySensing.ID, clusters.OccupancySensing.attributes.Occupancy.ID, nil) + }) + + local child_dimmer = test.mock_device.build_test_child_device({ + label = "Hager G2 4x Button 1", + profile = t_utils.get_profile_definition("light-level.yml"), + device_network_id = string.format("%s:3", parent_pir.id), + parent_device_id = parent_pir.id, + parent_assigned_child_key = "3" + }) + + test.mock_device.add_test_device(child_dimmer) + initiate_info_changed(child_dimmer, "light-level") + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.OnOff.ID, clusters.OnOff.attributes.OnOff.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.CurrentLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MaxLevel.ID, nil) + }) + test.socket.matter:__expect_send({ + parent_pir.id, + cluster_base.subscribe(parent_pir, nil, clusters.LevelControl.ID, clusters.LevelControl.attributes.MinLevel.ID, nil) + }) + test.wait_for_events() + --Verify motion detected event + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(parent_pir, 6, 1) + }) + + test.socket.capability:__expect_send(pir_device:generate_test_message("main", capabilities.motionSensor.motion.active())) + + -- Verify illuminance measurement event + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_report_data(parent_pir, 5, 21370) + }) + test.socket.capability:__expect_send(pir_device:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance(137))) + + -- Send on command to OnOff endpoint (dimmer) + child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switch", "on") + + test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switch", component = "main", command = "on", args = {} } }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.OnOff.commands.On(parent_pir, 3) + }) + -- Verify on state via attribute report + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.OnOff.attributes.OnOff:build_test_report_data(parent_pir, 3, true) + }) + test.socket.capability:__expect_send(child_dimmer:generate_test_message("main", capabilities.switch.switch.on())) + parent_pir.expect_native_attr_handler_registration(parent_pir, "switch", "switch") + + -- Set dimmer level to 50% + test.socket.capability:__queue_receive({ child_dimmer.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 50 } } }) + test.socket.matter:__expect_send({ + parent_pir.id, + clusters.LevelControl.commands.MoveToLevelWithOnOff(parent_pir, 3, 127, nil, 0, 0) + }) + child_dimmer.expect_native_cmd_handler_registration(child_dimmer, "switchLevel", "setLevel") + + -- Verify level via attribute report + test.socket.matter:__queue_receive({ + parent_pir.id, + clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(parent_pir, 3, 127) + }) + test.socket.capability:__expect_send(child_dimmer:generate_test_message("main", capabilities.switchLevel.level(50))) + parent_pir.expect_native_attr_handler_registration(parent_pir, "switchLevel", "level") +end) + +test.register_coroutine_test("Test: Host with Window Covering - 2-Button Profile with Window Covering Child Device", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local window_covering_device = create_matter_device_with_window("2-button", parent) + add_matter_device(window_covering_device, parent, "2-button") + test.wait_for_events() + configure_parent(parent) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(12), + })) + }) + + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) + }) + + -- DeviceTypeList report for window covering endpoint (type 514 = Window Covering Device) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { + { + device_type = data_types.Uint32(514), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "window-covering", + parent_device_id = parent.id, + parent_assigned_child_key = "12" + }) + + local child_wc = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("window-covering.yml"), + device_network_id = string.format("%s:12", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "12" + }) + + test.mock_device.add_test_device(child_wc) + initiate_info_changed(child_wc, "window-covering") + + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) + }) + + -- Open command + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "open", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.UpOrOpen(parent, 12) }) + test.wait_for_events() + + -- Verify open state via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.OperationalStatus:build_test_report_data(parent, 12, 0x01) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.opening())) + + + -- Close command + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "close", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.DownOrClose(parent, 12) }) + + -- Verify close state via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.OperationalStatus:build_test_report_data(parent, 12, 0x02) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.closing())) + + -- Pause command + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "pause", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.StopMotion(parent, 12) }) + test.wait_for_events() + + -- Set shade level to 50% + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 50 } } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 5000, nil, 0, 0) }) + test.wait_for_events() + + -- Verify shade level via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 5000) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(50))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.partially_open())) + + + -- Set shade level to 100% + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 100 } } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 0, nil, 0, 0) }) + test.wait_for_events() + + -- Verify shade level via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 0) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.open())) + + -- Set shade level to 0% + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 0 } } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.GoToLiftPercentage(parent, 12, 10000, nil, 0, 0) }) + + -- Verify shade level via attribute report + test.socket.matter:__queue_receive({ + parent.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data(parent, 12, 10000) + }) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0))) + test.socket.capability:__expect_send(child_wc:generate_test_message("main", capabilities.windowShade.windowShade.closed())) + +end) + +test.register_coroutine_test("Test: Window Covering - Preference Changes for Reverse Polarity and Preset Position", function() + test.socket.matter:__set_channel_ordering("relaxed") + add_parent_device(parent) + local window_covering_device = create_matter_device_with_window("2-button", parent) + add_matter_device(window_covering_device, parent, "2-button") + test.wait_for_events() + configure_parent(parent) + + test.wait_for_events() + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(12), + })) + }) + -- + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 8) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 9) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) + }) + test.wait_for_events() + + -- DeviceTypeList reports for button endpoints (type 15 = Generic Switch) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 8, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 9, data_types.Array { + { + device_type = data_types.Uint32(15), + revision = data_types.Uint16(1) + } + }) + }) + + -- DeviceTypeList report for window covering endpoint (type 514 = Window Covering Device) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { + { + device_type = data_types.Uint32(514), + revision = data_types.Uint16(1) + } + }) + }) + + parent:expect_device_create({ + type = "EDGE_CHILD", + label = "Hager G2 4x Button 1", + profile = "window-covering", + parent_device_id = parent.id, + parent_assigned_child_key = "12" + }) + + local child_wc = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("window-covering.yml"), + device_network_id = string.format("%s:12", parent.id), + parent_device_id = parent.id, + parent_assigned_child_key = "12" + }) + test.mock_device.add_test_device(child_wc) + + test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { reverse = "false" } })) + test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { reverse = "true" } })) + test.wait_for_events() + local reverse_preference_set = child_wc.preferences.reverse + assert(reverse_preference_set == "true", "reverse_preference_set is True") + -- + --Send open command - with reverse_polarity true, this should send DownOrClose + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "open", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.DownOrClose(parent, 12) }) + + -- Send close command - with reverse_polarity true, this should send UpOrOpen + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "close", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.UpOrOpen(parent, 12) }) + + -- Send Pause command + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShade", component = "main", command = "pause", args = {} } }) + test.socket.matter:__expect_send({ parent.id, clusters.WindowCovering.commands.StopMotion(parent, 12) }) + + -- Position preset testing + test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { presetPosition = "50" } })) + test.socket.device_lifecycle():__queue_receive(child_wc:generate_info_changed({ preferences = { presetPosition = "20" } })) + + test.wait_for_events() + + local PRESET_LEVEL_KEY = child_wc.preferences.presetPosition + assert(PRESET_LEVEL_KEY == "20", " presetPosition is set to 20") + + test.socket.capability:__queue_receive({ child_wc.id, { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } }) + test.socket.matter:__expect_send( + { parent.id, clusters.WindowCovering.server.commands.GoToLiftPercentage(parent, 12, 8000) } + ) + + test.wait_for_events() + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + })) + }) + + test.wait_for_events() + + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + test.mock_time.advance_time(5) + local BUTTON_EPS_FIELD = parent:get_field(BUTTON_EPS) + assert(BUTTON_EPS_FIELD[1] == 8) + assert(BUTTON_EPS_FIELD[2] == 9) + + test.wait_for_events() + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.PartsList:build_test_report_data(parent, 2, data_types.Array({ + data_types.Uint16(8), + data_types.Uint16(9), + data_types.Uint16(12), + })) + }) + test.socket.matter:__expect_send({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:subscribe(parent, 12) + }) + test.socket.matter:__queue_receive({ + parent.id, + clusters.Descriptor.attributes.DeviceTypeList:build_test_report_data(parent, 12, data_types.Array { + { + device_type = data_types.Uint32(514), + revision = data_types.Uint16(1) + } + }) + }) + child_wc:expect_metadata_update({ profile = "window-covering" }) + initiate_info_changed(child_wc, "window-covering") + + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.OperationalStatus.ID, nil) + }) + test.socket.matter:__expect_send({ + parent.id, + cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) + }) +end) + +test.run_registered_tests() From 4b77007f933b15db3ab448de01aa4a4928b5ad20 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:57:20 +0800 Subject: [PATCH 21/24] add firstled-io_M3S3BAC (#3053) Co-authored-by: Chris Baumler --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 5 +++++ .../zigbee-switch/src/firstled-io/fingerprints.lua | 1 + tools/localizations/cn.csv | 1 + 3 files changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index fbd5adaaf0..439bb5da28 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2457,6 +2457,11 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M4S4BAC deviceProfileName: switch-light-restore-wireless + - id: "FIRSTLED/M3S3BAC" + deviceLabel: Mirror Series 3x3 1 + manufacturer: FIRSTLED + model: M3S3BAC + deviceProfileName: switch-light-restore-wireless - id: "FIRSTLED/P3S3BAC" deviceLabel: Punk Series 3x3 1 manufacturer: FIRSTLED diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua index 5b50a09368..1b5722fa7c 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-io/fingerprints.lua @@ -6,6 +6,7 @@ --The driver supports a series of device combinations, such as 4+4, 3+3, 2+2, 4+0, etc., of switch and button type products. return { { mfr = "FIRSTLED", model = "M4S4BAC", children = 4, buttons = 4, child_profile = "switch-wireless" }, + { mfr = "FIRSTLED", model = "M3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "P3S3BAC", children = 3, buttons = 3, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "P2S2BAC", children = 2, buttons = 2, child_profile = "switch-wireless" }, { mfr = "FIRSTLED", model = "P1S1BAC", children = 1, buttons = 1, child_profile = "switch-wireless" }, diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index f23ee9fe68..6c6aac0a81 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,6 +141,7 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "MultiIR Smoke Detector MIR-SM200",麦乐克烟雾报警器MIR-SM200 "MultiIR Siren MIR-SR100",麦乐克声光报警器MIR-SR100 "Mirror Series 4x4 1",镜系列4x4 1 +"Mirror Series 3x3 1",镜系列3x3 1 "Punk Series 3x3 1",朋克系列3x3 1 "Punk Series 2x2 1",朋克系列2x2 1 "Punk Series 1x1 1",朋克系列1x1 1 From 841fd6fd2ad47ef8db8c1c6f16439f30d1e0ae94 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:15:46 -0500 Subject: [PATCH 22/24] Zigbee Humidity Sensor: Add category to profile (#3117) --- .../profiles/frient-airquality-humidity-temperature-battery.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml index 08774c0d31..1df453e4aa 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/profiles/frient-airquality-humidity-temperature-battery.yml @@ -27,6 +27,8 @@ components: version: 1 - id: refresh version: 1 + categories: + - name: AirQualityDetector preferences: - preferenceId: humidityOffset explicit: true From cc0a3afe9f51fbe7ba14fb89a58eeed515f97e83 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Tue, 21 Jul 2026 12:32:52 -0500 Subject: [PATCH 23/24] WWSTCERT-12695 Smart WiFi Outdoor Plug (#3123) --- drivers/SmartThings/matter-switch/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index d6a367c685..6a4321c1a5 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -4199,6 +4199,11 @@ matterManufacturer: vendorId: 0x1392 productId: 0x0106 deviceProfileName: plug-power-energy-powerConsumption + - id: "5010/276" + deviceLabel: Smart WiFi Outdoor Plug + vendorId: 0x1392 + productId: 0x0114 + deviceProfileName: plug-energy-powerConsumption #GIANT LIGHTING - id: "5707/1" deviceLabel: Floor Lamp From 282a3f160db05150b962eab6e5e9e09d62af0f66 Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:24:10 +0900 Subject: [PATCH 24/24] Matter Lock: Update deprecated fingerprints (#3138) --------- Signed-off-by: Hunsup Jung --- .../SmartThings/matter-lock/fingerprints.yml | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index bfa19905a5..75f2fcf37d 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -117,48 +117,48 @@ matterManufacturer: deviceLabel: Level Lock Plus (Matter) vendorId: 0x129F productId: 0x0001 - deviceProfileName: lock-nocodes-notamper-batteryLevel + deviceProfileName: lock - id: "4767/3" deviceLabel: Level Bolt (Matter) vendorId: 0x129F productId: 0x0003 - deviceProfileName: lock-nocodes-notamper-batteryLevel + deviceProfileName: lock - id: "4767/4" deviceLabel: Level Lock Pro vendorId: 0x129F productId: 0x0004 - deviceProfileName: lock-nocodes-notamper-batteryLevel + deviceProfileName: lock - id: "4767/5" deviceLabel: Level Lock (Matter) vendorId: 0x129F productId: 0x0005 - deviceProfileName: lock-nocodes-notamper-batteryLevel + deviceProfileName: lock #Nuki - id: "4957/161" deviceLabel: Nuki Smart Lock Ultra vendorId: 0x135D productId: 0x00A1 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock - id: "Nuki Smart Lock" deviceLabel: Nuki Smart Lock vendorId: 0x135d productId: 0xb0 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock - id: "Nuki Smart Lock Pro" deviceLabel: Nuki Smart Lock Pro vendorId: 0x135d productId: 0xb1 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock - id: "4957/178" deviceLabel: Nuki Smart Lock Pro vendorId: 0x135D productId: 0x00B2 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock - id: "4957/193" deviceLabel: Nuki Smart Lock Go vendorId: 0x135D productId: 0x00C1 - deviceProfileName: lock-nocodes-notamper + deviceProfileName: lock #U-tec - id: "5247/1" deviceLabel: ULTRALOQ Bolt Fingerprint Smart Matter Deadbolt @@ -202,4 +202,4 @@ matterGeneric: deviceLabel: Matter Door Lock deviceTypes: - id: 0x000A # Door Lock - deviceProfileName: base-lock + deviceProfileName: lock