From d973a4eb74395ac295065f9665ae7d9a4c3409e7 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 24 Jun 2026 13:58:38 -0500 Subject: [PATCH 001/102] Adding HCv0.61 to Jenkins driver test matrix --- .github/workflows/jenkins-driver-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jenkins-driver-tests.yml b/.github/workflows/jenkins-driver-tests.yml index b9fc07f091..6494eadd15 100644 --- a/.github/workflows/jenkins-driver-tests.yml +++ b/.github/workflows/jenkins-driver-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: version: - [ 59, 60, dev] + [ 59, 60, 61, dev] runs-on: ubuntu-latest steps: From 1a1bca77f2700110b963ff95d591cb15cbb2cd21 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 25 Jun 2026 11:22:07 -0500 Subject: [PATCH 002/102] WWSTCERT-12419 Dual Socket Smart Plug (#3046) --- 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 e4602b4178..b17f2578cb 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -557,6 +557,11 @@ matterManufacturer: vendorId: 0x1339 productId: 0x0056 deviceProfileName: light-color-level + - id: "4921/85" + deviceLabel: Dual Socket Smart Plug + vendorId: 0x1339 + productId: 0x0055 + deviceProfileName: plug-binary #Govee - id: "4999/24740" deviceLabel: Govee Square Ceiling Light (12 inch) From 47443d1cf4ab5bf1073e61d3d5463b85b3513bc6 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 25 Jun 2026 11:44:25 -0500 Subject: [PATCH 003/102] WWSTCERT-12246 Floor Lamp (#3047) --- drivers/SmartThings/matter-switch/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index b17f2578cb..8637b37713 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -4174,6 +4174,12 @@ matterManufacturer: vendorId: 0x1392 productId: 0x0106 deviceProfileName: plug-power-energy-powerConsumption +#GIANT LIGHTING + - id: "5707/1" + deviceLabel: Floor Lamp + vendorId: 0x164B + productId: 0x0001 + deviceProfileName: light-color-level matterGeneric: From 2ecb5a9526afb2deda800b204987028d905c08b5 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 25 Jun 2026 12:02:21 -0500 Subject: [PATCH 004/102] WWSTCERT-12397 GELUBU Door Lock R60 (#3041) --- .../SmartThings/zigbee-lock/fingerprints.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/SmartThings/zigbee-lock/fingerprints.yml b/drivers/SmartThings/zigbee-lock/fingerprints.yml index 083dd3a11e..309a0f2883 100644 --- a/drivers/SmartThings/zigbee-lock/fingerprints.yml +++ b/drivers/SmartThings/zigbee-lock/fingerprints.yml @@ -10,6 +10,26 @@ zigbeeManufacturer: manufacturer: GELUBU model: G30 deviceProfileName: lock-battery + - id: "GELUBU/R60" + deviceLabel: GELUBU Door Lock R60 + manufacturer: GELUBU + model: R60 + deviceProfileName: lock-battery + - id: "GELUBU/R50" + deviceLabel: GELUBU Door Lock R50 + manufacturer: GELUBU + model: R50 + deviceProfileName: lock-battery + - id: "GELUBU/P90" + deviceLabel: GELUBU Door Lock P90 + manufacturer: GELUBU + model: P90 + deviceProfileName: lock-battery + - id: "GELUBU/R71" + deviceLabel: GELUBU Door Lock R71 + manufacturer: GELUBU + model: R71 + deviceProfileName: lock-battery # YALE - id: "Yale YRD220/240" deviceLabel: "Yale Door Lock" From dae7b10c336e69ea87ac848edc893d9d2fbf64d3 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Thu, 25 Jun 2026 11:36:39 -0500 Subject: [PATCH 005/102] firstled_switch: Updated min_api_version to 20 for out of date tests --- .../src/test/test_firstled_switch.lua | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_firstled_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_firstled_switch.lua index 3a3c4782ea..a9bc2ac8fe 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_firstled_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_firstled_switch.lua @@ -195,7 +195,7 @@ test.register_coroutine_test("infoChanged - backlight 0", function() }) end, { - min_api_version = 19 + min_api_version = 20 } ) @@ -206,7 +206,7 @@ test.register_coroutine_test("infoChanged - backlight 1", function() }) end, { - min_api_version = 19 + min_api_version = 20 } ) @@ -217,7 +217,7 @@ test.register_coroutine_test("infoChanged - backlight 2", function() }) end, { - min_api_version = 19 + min_api_version = 20 } ) @@ -228,18 +228,18 @@ test.register_coroutine_test("infoChanged - powerOnStatus 0", function() }) end, { - min_api_version = 19 + min_api_version = 20 } ) -test.register_coroutine_test("infoChanged - powerOnStatus", function() +test.register_coroutine_test("infoChanged - powerOnStatus 1", function() test.socket.device_lifecycle:__queue_receive(mock_parent:generate_info_changed({ preferences = { powerOnStatus = "1" }})) test.socket.zigbee:__expect_send({ mock_parent.id, cluster_base.write_manufacturer_specific_attribute(mock_parent, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE, data_types.Uint8, 1) }) end, { - min_api_version = 19 + min_api_version = 20 } ) @@ -250,7 +250,7 @@ test.register_coroutine_test("infoChanged - powerOnStatus 2", function() }) end, { - min_api_version = 19 + min_api_version = 20 } ) @@ -262,7 +262,7 @@ test.register_coroutine_test("infoChanged - stse.changeToWirelessSwitch true", f mock_parent:expect_metadata_update({ profile = "switch-button-light-restore-wireless" }) end, { - min_api_version = 19 + min_api_version = 20 } ) @@ -274,7 +274,7 @@ test.register_coroutine_test("infoChanged - stse.changeToWirelessSwitch false", mock_parent:expect_metadata_update({ profile = "switch-light-restore-wireless" }) end, { - min_api_version = 19 + min_api_version = 20 } ) @@ -287,7 +287,7 @@ local function test_child_changeToWirelessSwitch_true(ep, name) mock_children[ep]:expect_metadata_update({ profile = "switch-button-wireless" }) end, { - min_api_version = 19 + min_api_version = 20 } ) end @@ -301,7 +301,7 @@ local function test_child_changeToWirelessSwitch_false(ep, name) mock_children[ep]:expect_metadata_update({ profile = "switch-wireless" }) end, { - min_api_version = 19 + min_api_version = 20 } ) end From 4d9d3999ed551dc1b851c19ffcea1de585dae8b1 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:31:06 -0500 Subject: [PATCH 006/102] Matter Sensor: Fix nil value logging and handling for soil sensor attribute (#3073) --- .../matter-sensor/src/sensor_handlers/attribute_handlers.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua index a81a03b4a6..eaa8730fc4 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua @@ -89,7 +89,8 @@ function AttributeHandlers.soil_moisture_measurement_limits_handler(driver, devi local min_val = ib.data.elements and ib.data.elements.min_measured_value and ib.data.elements.min_measured_value.value local max_val = ib.data.elements and ib.data.elements.max_measured_value and ib.data.elements.max_measured_value.value if not (min_val and max_val) or (min_val >= max_val) or (min_val < sensor_utils.SOIL_MOISTURE_MIN) or (max_val > sensor_utils.SOIL_MOISTURE_MAX) then - device.log.warn_with({hub_logs = true}, string.format("Device reported invalid soil moisture limits: min=%d, max=%d", min_val, max_val)) + device.log.warn_with({hub_logs = true}, string.format("Device reported invalid soil moisture limits: min=%s, max=%s", min_val, max_val)) + return end sensor_utils.set_field_for_endpoint(device, fields.SOIL_LIMIT_MIN, ib.endpoint_id, min_val) sensor_utils.set_field_for_endpoint(device, fields.SOIL_LIMIT_MAX, ib.endpoint_id, max_val) From 0d148565304be56b2488f89c2a12910c5057dedd Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 29 Jun 2026 08:53:09 -0500 Subject: [PATCH 007/102] Hue: fix conditional checking in hue command error logging (#3072) --- .../philips-hue/src/handlers/commands.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/handlers/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua index 6bee932239..2c55f39452 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua @@ -54,13 +54,14 @@ end ---@param err string? Error message returned from the Hue API call, if any ---@param action_desc string Description of the action being performed, for logging purposes local function log_command_response_errors(response, err, action_desc) - if not response or (response.errors and #response.errors == 0) then - if err ~= nil then - log.error_with({ hub_logs = true }, "Error performing " .. action_desc .. ": " .. err) - elseif response and #response.errors > 0 then - for _, error in ipairs(response.errors) do - log.error_with({ hub_logs = true }, "Error returned in Hue response for " .. action_desc .. ": " .. error.description) - end + if err ~= nil then + log.error_with({ hub_logs = true }, "Error performing " .. action_desc .. ": " .. err) + end + + if response and response.errors and #response.errors > 0 then + for _, error in ipairs(response.errors) do + log.error_with({ hub_logs = true }, + "Error returned in Hue response for " .. action_desc .. ": " .. error.description) end end end From 42520391710afb86f348d873dfdf5ef5fab9037a Mon Sep 17 00:00:00 2001 From: "Jan Jakubiszyn/SmartThings Integrations (BJ.jakubiszynS) /SRPOL/Engineer/Samsung Electronics" Date: Mon, 30 Mar 2026 18:30:39 +0200 Subject: [PATCH 008/102] 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 8637b37713..222950789b 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -904,6 +904,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 e2a04e103a..3a8f32f345 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_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 02c864fa4f..b0aca9d102 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 feccb91b9b801e1959fdcd8c7dcaf7f120c14944 Mon Sep 17 00:00:00 2001 From: JanJakubiszyn <101173721+JanJakubiszyn@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:11:56 +0200 Subject: [PATCH 009/102] 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 222950789b..8637b37713 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -904,26 +904,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 3a8f32f345..e2a04e103a 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_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 b0aca9d102..02c864fa4f 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 65a2532558893b02b8e2e6e4862148f545fcf707 Mon Sep 17 00:00:00 2001 From: r-slezak Date: Thu, 2 Jul 2026 17:08:24 +0200 Subject: [PATCH 010/102] Add support for Tuya thermostats (#3071) * Add support for Tuya thermostats * Add more unit tests * Update copyright notice in test_tuya_thermostat.lua Updated copyright information in test_tuya_thermostat.lua. * Update copyright and license comments in init.lua Updated copyright notice and licensing information. * Update test_tuya_thermostat.lua * Update drivers/Unofficial/tuya-zigbee/src/thermostat/init.lua Co-authored-by: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> * Remove thermostatMode enabledValues from profile * Move category to be below capabilities in Tuya Zigbee thermostat profile * Add newline at end of Tuya Zigbee fingerprints --------- Co-authored-by: Konrad K <33450498+KKlimczukS@users.noreply.github.com> Co-authored-by: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> --- .../Unofficial/tuya-zigbee/fingerprints.yml | 7 +- .../tuya-zigbee/profiles/thermostat.yml | 24 + drivers/Unofficial/tuya-zigbee/src/init.lua | 3 +- .../src/test/test_tuya_thermostat.lua | 545 ++++++++++++++++++ .../tuya-zigbee/src/thermostat/init.lua | 187 ++++++ 5 files changed, 764 insertions(+), 2 deletions(-) create mode 100644 drivers/Unofficial/tuya-zigbee/profiles/thermostat.yml create mode 100644 drivers/Unofficial/tuya-zigbee/src/test/test_tuya_thermostat.lua create mode 100644 drivers/Unofficial/tuya-zigbee/src/thermostat/init.lua diff --git a/drivers/Unofficial/tuya-zigbee/fingerprints.yml b/drivers/Unofficial/tuya-zigbee/fingerprints.yml index 7ee7460696..5c77617d1d 100644 --- a/drivers/Unofficial/tuya-zigbee/fingerprints.yml +++ b/drivers/Unofficial/tuya-zigbee/fingerprints.yml @@ -43,4 +43,9 @@ zigbeeManufacturer: deviceLabel: Tuya Switch 1 manufacturer: _TZE204_h2rctifa model: TS0601 - deviceProfileName: basic-switch \ No newline at end of file + deviceProfileName: basic-switch + - id: _TZE284_fziifcxj/TS0601 + deviceLabel: Tuya Thermostat + manufacturer: _TZE284_fziifcxj + model: TS0601 + deviceProfileName: thermostat diff --git a/drivers/Unofficial/tuya-zigbee/profiles/thermostat.yml b/drivers/Unofficial/tuya-zigbee/profiles/thermostat.yml new file mode 100644 index 0000000000..66ecb5661b --- /dev/null +++ b/drivers/Unofficial/tuya-zigbee/profiles/thermostat.yml @@ -0,0 +1,24 @@ +name: thermostat +components: + - id: main + capabilities: + - id: temperatureMeasurement + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + config: + values: + - key: "heatingSetpoint.value" + range: [5, 30] + step: 0.5 + - id: thermostatMode + version: 1 + config: + values: + - key: "thermostatMode.value" + - id: battery + version: 1 + - id: refresh + version: 1 + categories: + - name: Thermostat diff --git a/drivers/Unofficial/tuya-zigbee/src/init.lua b/drivers/Unofficial/tuya-zigbee/src/init.lua index 8fb452abb2..6313132933 100644 --- a/drivers/Unofficial/tuya-zigbee/src/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/init.lua @@ -26,7 +26,8 @@ local unofficial_tuya_driver_template = { require("curtain"), require("motion-sensor"), require("smoke-detector"), - require("switch") + require("switch"), + require("thermostat") }, health_check = false, } diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_thermostat.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_thermostat.lua new file mode 100644 index 0000000000..4df634bf18 --- /dev/null +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_thermostat.lua @@ -0,0 +1,545 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Mock out globals +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local tuya_utils = require "tuya_utils" + +local Basic = clusters.Basic + +local mock_simple_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("thermostat.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "_TZE284_fziifcxj", + model = "TS0601", + server_clusters = { 0xEF00 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_simple_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Handle doConfigure lifecycle event", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ mock_simple_device.id, tuya_utils.build_tuya_magic_spell_message(mock_simple_device) }) + test.socket.zigbee:__expect_send({ mock_simple_device.id, Basic.attributes.ApplicationVersion:configure_reporting(mock_simple_device, 30, 300, 1) }) + test.socket.zigbee:__expect_send({ mock_simple_device.id, zigbee_test_utils.build_bind_request(mock_simple_device, zigbee_test_utils.mock_hub_eui, Basic.ID) }) + mock_simple_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + {} +) + +test.register_coroutine_test( + "Handle added lifecycle event", + function() + test.socket.device_lifecycle:__queue_receive({ mock_simple_device.id, "added" }) + + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message( + "main", + capabilities.thermostatMode.supportedThermostatModes( + { + capabilities.thermostatMode.thermostatMode.antifreezing.NAME, + capabilities.thermostatMode.thermostatMode.auto.NAME, + capabilities.thermostatMode.thermostatMode.comfort.NAME, + capabilities.thermostatMode.thermostatMode.eco.NAME, + capabilities.thermostatMode.thermostatMode.off.NAME, + capabilities.thermostatMode.thermostatMode.on.NAME, + }, + { + visibility = { displayed = false } + } + ) + ) + ) + + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message( + "main", + capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 15.0, unit = "C"}) + ) + ) + + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message( + "main", + capabilities.temperatureMeasurement.temperature({value = 20.0, unit = "C"}) + ) + ) + + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message( + "main", + capabilities.thermostatMode.thermostatMode.auto() + ) + ) + + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message( + "main", + capabilities.battery.battery(100) + ) + ) + end, + {} +) + +test.register_message_test( + "Handle thermostatHeatingSetpoint setHeatingSetpoint", + { + { + channel = "capability", + direction = "receive", + message = { + mock_simple_device.id, + { + capability = "thermostatHeatingSetpoint", + component = "main", + command = "setHeatingSetpoint", + args = {12.5} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_simple_device.id, + tuya_utils.build_send_tuya_command( + mock_simple_device, + "\x04", + tuya_utils.DP_TYPE_VALUE, + "\x00\x00\x00\x7D", + 0x00 + ) + } + } + }, + {} +) + +test.register_message_test( + "Handle thermostatMode setThermostatMode (auto)", + { + { + channel = "capability", + direction = "receive", + message = { + mock_simple_device.id, + { + capability = "thermostatMode", + component = "main", + command = "setThermostatMode", + args = {"auto"} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_simple_device.id, + tuya_utils.build_send_tuya_command( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x00", + 0x00 + ) + } + } + }, + {} +) + +test.register_message_test( + "Handle thermostatMode setThermostatMode (off)", + { + { + channel = "capability", + direction = "receive", + message = { + mock_simple_device.id, + { + capability = "thermostatMode", + component = "main", + command = "setThermostatMode", + args = {"off"} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_simple_device.id, + tuya_utils.build_send_tuya_command( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x01", + 0x00 + ) + } + } + }, + {} +) + +test.register_message_test( + "Handle thermostatMode setThermostatMode (on)", + { + { + channel = "capability", + direction = "receive", + message = { + mock_simple_device.id, + { + capability = "thermostatMode", + component = "main", + command = "setThermostatMode", + args = {"on"} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_simple_device.id, + tuya_utils.build_send_tuya_command( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x02", + 0x00 + ) + } + } + }, + {} +) + +test.register_message_test( + "Handle thermostatMode setThermostatMode (comfort)", + { + { + channel = "capability", + direction = "receive", + message = { + mock_simple_device.id, + { + capability = "thermostatMode", + component = "main", + command = "setThermostatMode", + args = {"comfort"} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_simple_device.id, + tuya_utils.build_send_tuya_command( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x03", + 0x00 + ) + } + } + }, + {} +) + +test.register_message_test( + "Handle thermostatMode setThermostatMode (eco)", + { + { + channel = "capability", + direction = "receive", + message = { + mock_simple_device.id, + { + capability = "thermostatMode", + component = "main", + command = "setThermostatMode", + args = {"eco"} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_simple_device.id, + tuya_utils.build_send_tuya_command( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x04", + 0x00 + ) + } + } + }, + {} +) + +test.register_message_test( + "Handle thermostatMode setThermostatMode (antifreezing)", + { + { + channel = "capability", + direction = "receive", + message = { + mock_simple_device.id, + { + capability = "thermostatMode", + component = "main", + command = "setThermostatMode", + args = {"antifreezing"} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_simple_device.id, + tuya_utils.build_send_tuya_command( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x05", + 0x00 + ) + } + } + }, + {} +) + +test.register_message_test( + "Handle tuya cluster message report (heatingSetpoint)", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_simple_device.id, + tuya_utils.build_test_attr_report( + mock_simple_device, + "\x04", + tuya_utils.DP_TYPE_VALUE, + "\x00\x00\x00\x7D", + 0x01 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_simple_device:generate_test_message( + "main", + capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 12.5, unit = "C"}) + ) + } + }, + {} +) + +test.register_message_test( + "Handle tuya cluster message report (setThermostatMode, auto)", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_simple_device.id, + tuya_utils.build_test_attr_report( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x00", + 0x01 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_simple_device:generate_test_message( + "main", + capabilities.thermostatMode.thermostatMode.auto() + ) + } + }, + {} +) + +test.register_message_test( + "Handle tuya cluster message report (setThermostatMode, off)", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_simple_device.id, + tuya_utils.build_test_attr_report( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x01", + 0x01 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_simple_device:generate_test_message( + "main", + capabilities.thermostatMode.thermostatMode.off() + ) + } + }, + {} +) + +test.register_message_test( + "Handle tuya cluster message report (setThermostatMode, on)", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_simple_device.id, + tuya_utils.build_test_attr_report( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x02", + 0x01 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_simple_device:generate_test_message( + "main", + capabilities.thermostatMode.thermostatMode.on() + ) + } + }, + {} +) + +test.register_message_test( + "Handle tuya cluster message report (setThermostatMode, comfort)", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_simple_device.id, + tuya_utils.build_test_attr_report( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x03", + 0x01 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_simple_device:generate_test_message( + "main", + capabilities.thermostatMode.thermostatMode.comfort() + ) + } + }, + {} +) + +test.register_message_test( + "Handle tuya cluster message report (setThermostatMode, eco)", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_simple_device.id, + tuya_utils.build_test_attr_report( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x04", + 0x01 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_simple_device:generate_test_message( + "main", + capabilities.thermostatMode.thermostatMode.eco() + ) + } + }, + {} +) + +test.register_message_test( + "Handle tuya cluster message report (setThermostatMode, antifreezing)", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_simple_device.id, + tuya_utils.build_test_attr_report( + mock_simple_device, + "\x02", + tuya_utils.DP_TYPE_ENUM, + "\x05", + 0x01 + ) + } + }, + { + channel = "capability", + direction = "send", + message = mock_simple_device:generate_test_message( + "main", + capabilities.thermostatMode.thermostatMode.antifreezing() + ) + } + }, + {} +) + +test.run_registered_tests() diff --git a/drivers/Unofficial/tuya-zigbee/src/thermostat/init.lua b/drivers/Unofficial/tuya-zigbee/src/thermostat/init.lua new file mode 100644 index 0000000000..2179fb28a4 --- /dev/null +++ b/drivers/Unofficial/tuya-zigbee/src/thermostat/init.lua @@ -0,0 +1,187 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local defaults = require "st.zigbee.defaults" +local device_management = require "st.zigbee.device_management" +local clusters = require "st.zigbee.zcl.clusters" +local tuya_utils = require "tuya_utils" +local Basic = clusters.Basic +local packet_id = 0 + +local FINGERPRINTS = { + { mfr = "_TZE284_fziifcxj", model = "TS0601"} +} + +local function is_tuya_thermostat(opts, driver, device) + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function device_added(self, device) + device:emit_event(capabilities.thermostatMode.supportedThermostatModes({ + capabilities.thermostatMode.thermostatMode.antifreezing.NAME, + capabilities.thermostatMode.thermostatMode.auto.NAME, + capabilities.thermostatMode.thermostatMode.comfort.NAME, + capabilities.thermostatMode.thermostatMode.eco.NAME, + capabilities.thermostatMode.thermostatMode.off.NAME, + capabilities.thermostatMode.thermostatMode.on.NAME, + }, { visibility = { displayed = false } })) + device:emit_event(capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 15.0, unit = "C"})) + device:emit_event(capabilities.temperatureMeasurement.temperature({value = 20.0, unit = "C"})) + device:emit_event(capabilities.thermostatMode.thermostatMode.auto()) + device:emit_event(capabilities.battery.battery(100)) +end + +local function do_configure(driver, device) + -- configure ApplicationVersion to keep device online, tuya hub also uses this attribute + tuya_utils.send_magic_spell(device) + device:send(Basic.attributes.ApplicationVersion:configure_reporting(device, 30, 300, 1)) + device:send(device_management.build_bind_request( + device, + Basic.ID, + driver.environment_info.hub_zigbee_eui + )) +end + +local function increase_packet_id(pid) + return (pid + 1) % 65536 +end + +local function do_refresh(driver, device) + device.log.info("do_refresh called") +end + +local MODE_MAP = { + [capabilities.thermostatMode.thermostatMode.auto.NAME] = "\x00", + [capabilities.thermostatMode.thermostatMode.off.NAME] = "\x01", + [capabilities.thermostatMode.thermostatMode.on.NAME] = "\x02", + [capabilities.thermostatMode.thermostatMode.comfort.NAME] = "\x03", + [capabilities.thermostatMode.thermostatMode.eco.NAME] = "\x04", + [capabilities.thermostatMode.thermostatMode.antifreezing.NAME] = "\x05", +} + +local function set_thermostat_mode(driver, device, command) + local mode_value = MODE_MAP[command.args.mode] + if mode_value ~= nil then + tuya_utils.send_tuya_command(device, "\x02", tuya_utils.DP_TYPE_ENUM, mode_value, packet_id) + packet_id = increase_packet_id(packet_id) + end +end + +local function set_heating_setpoint(driver, device, command) + local value = command.args.setpoint + local setpoint_raw = math.floor(value * 10 + 0.5) + tuya_utils.send_tuya_command( + device, + "\x04", + tuya_utils.DP_TYPE_VALUE, + string.pack(">I4", setpoint_raw), + packet_id + ) + packet_id = increase_packet_id(packet_id) +end + +local function tuya_cluster_handler(driver, device, zb_rx) + local event + local raw = zb_rx.body.zcl_body.body_bytes + local dp = raw:byte(3) + local dp_type = raw:byte(4) + local dp_data_len = string.unpack(">I2", raw:sub(5, 6)) + local dp_data = raw:sub(7, 6 + dp_data_len) + + if dp == 0x04 then -- Target temperature + if dp_type == 0x02 and dp_data_len >= 4 then -- value + local target_temp_raw = string.unpack(">I4", dp_data:sub(1, 4)) + local target_temp = target_temp_raw / 10.0 + event = capabilities.thermostatHeatingSetpoint.heatingSetpoint({ + value = target_temp, + unit = "C" + }) + end + + elseif dp == 0x05 then -- Current temperature + if dp_type == 0x02 and dp_data_len >= 4 then -- value + local temp_raw = string.unpack(">I4", dp_data:sub(1, 4)) + local temp = temp_raw / 10.0 + event = capabilities.temperatureMeasurement.temperature({ + value = temp, + unit = "C" + }) + end + + elseif dp == 0x02 then -- Thermostat mode + if dp_type == 0x04 and dp_data_len >= 1 then -- enum + local mode = dp_data:byte(1) + if mode == 0x00 then -- auto + event = capabilities.thermostatMode.thermostatMode.auto() + elseif mode == 0x01 then -- off + event = capabilities.thermostatMode.thermostatMode.off() + elseif mode == 0x02 then -- on + event = capabilities.thermostatMode.thermostatMode.on() + elseif mode == 0x03 then -- comfort + event = capabilities.thermostatMode.thermostatMode.comfort() + elseif mode == 0x04 then -- eco + event = capabilities.thermostatMode.thermostatMode.eco() + elseif mode == 0x05 then -- antifreezing + event = capabilities.thermostatMode.thermostatMode.antifreezing() + end + end + + elseif dp == 0x06 then -- Battery level + if dp_type == 0x02 and dp_data_len >= 4 then -- value + local battery_level = string.unpack(">I4", dp_data:sub(1, 4)) + event = capabilities.battery.battery(battery_level) + end + end + + if event ~= nil then + device:emit_event(event) + end +end + +local tuya_thermostat_driver = { + NAME = "tuya thermostat", + supported_capabilities = { + capabilities.temperatureMeasurement, + capabilities.thermostatHeatingSetpoint, + capabilities.thermostatMode, + capabilities.battery, + capabilities.refresh + }, + zigbee_handlers = { + cluster = { + [tuya_utils.TUYA_PRIVATE_CLUSTER] = { + [tuya_utils.TUYA_PRIVATE_CMD_REPORT] = tuya_cluster_handler, + [tuya_utils.TUYA_PRIVATE_CMD_RESPONSE] = tuya_cluster_handler, + } + } + }, + capability_handlers = { + [capabilities.thermostatMode.ID] = { + [capabilities.thermostatMode.commands.setThermostatMode.NAME] = set_thermostat_mode, + }, + [capabilities.thermostatHeatingSetpoint.ID] = { + [capabilities.thermostatHeatingSetpoint.commands.setHeatingSetpoint.NAME] = set_heating_setpoint, + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + }, + }, + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure + }, + can_handle = is_tuya_thermostat +} + +defaults.register_for_default_handlers( + tuya_thermostat_driver, + tuya_thermostat_driver.supported_capabilities, + {} +) +return tuya_thermostat_driver From 6bae4060e1d9012cfecadfe26c1443d0a435a01d Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Fri, 3 Jul 2026 00:36:19 +0800 Subject: [PATCH 011/102] add firstled light Easy-Fit Driver (#3031) * add firstled light Easy-Fit Driver * add caps LevelStep ColorTemperatureStep --------- Co-authored-by: Chris Baumler --- .../zigbee-switch/fingerprints.yml | 5 + .../light-color-temp-time-restore.yml | 74 ++++++++++++ .../src/firstled-light/can_handle.lua | 13 +++ .../src/firstled-light/fingerprints.lua | 6 + .../zigbee-switch/src/firstled-light/init.lua | 63 +++++++++++ .../zigbee-switch/src/sub_drivers.lua | 3 +- .../src/test/test_firstled_light.lua | 106 ++++++++++++++++++ 7 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-switch/profiles/light-color-temp-time-restore.yml create mode 100644 drivers/SmartThings/zigbee-switch/src/firstled-light/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/firstled-light/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/firstled-light/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_firstled_light.lua diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index b44a13d3c1..1d67094618 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: "ManDai Technology/DC2DC12MiV1" + deviceLabel: Easy-Fit Driver + manufacturer: ManDai Technology + model: DC2DC12MiV1 + deviceProfileName: light-color-temp-time-restore zigbeeGeneric: - id: "genericSwitch" deviceLabel: Zigbee Switch diff --git a/drivers/SmartThings/zigbee-switch/profiles/light-color-temp-time-restore.yml b/drivers/SmartThings/zigbee-switch/profiles/light-color-temp-time-restore.yml new file mode 100644 index 0000000000..45fd965a6c --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/light-color-temp-time-restore.yml @@ -0,0 +1,74 @@ +name: light-color-temp-time-restore +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + config: + values: + - key: "level.value" + range: [1, 100] + - id: statelessSwitchLevelStep + version: 1 + - id: colorTemperature + version: 1 + - id: statelessColorTemperatureStep + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: Light +preferences: + - title: "上电状态(power-on state/파워온 상태)" + name: powerOnMode + description: "上电状态(power-on state/파워온 상태)" + required: false + preferenceType: enumeration + definition: + options: + 0: "关闭(off/닫다)" + 1: "打开(on/열다)" + 2: "恢复记忆状态(restore/복원)" + default: 0 + - title: "驱动电流(drive current/구동 전류)" + name: ledDriveCurrent + description: "驱动电流(drive current/구동 전류)/50~333mA" + required: false + preferenceType: integer + definition: + minimum: 50 + maximum: 333 + default: 270 + - title: "亮度时间(dimmingTransitionTime/밝기 전환 시간)" + name: dimTransitionTime + description: "亮度过渡时间(dimming transition time/밝기 전환 시간)1000~10000ms" + required: false + preferenceType: integer + definition: + minimum: 1000 + maximum: 10000 + default: 1000 + - title: "色温时间(CCTTransitionTime/색온 전환 시간)" + name: colorTempTransitionTime + description: "色温过渡时间(CCT transition time/색온 전환 시간)1000~10000ms" + required: false + preferenceType: integer + definition: + minimum: 1000 + maximum: 10000 + default: 1000 + - title: "输出模式(output mode/출력 모드)" + name: outputMode + description: "输出模式(output mode/출력 모드)" + required: false + preferenceType: enumeration + definition: + options: + 0: "双路CW(dual CW/듀얼 CW)" + 1: "双路WC(dual WC/듀얼 WC)" + 2: "单路(single channel/단일 채널)" + default: 0 diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-light/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/firstled-light/can_handle.lua new file mode 100644 index 0000000000..6b02320ebf --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/firstled-light/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 FINGERPRINTS = require "firstled-light.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("firstled-light") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-light/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-light/fingerprints.lua new file mode 100644 index 0000000000..ba8c2347b3 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/firstled-light/fingerprints.lua @@ -0,0 +1,6 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "ManDai Technology", model = "DC2DC12MiV1" } +} diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-light/init.lua b/drivers/SmartThings/zigbee-switch/src/firstled-light/init.lua new file mode 100644 index 0000000000..ab81b0f0cf --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/firstled-light/init.lua @@ -0,0 +1,63 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local PRIVATE_CLUSTER_ID = 0xFC00 + +local preference_map = { + ["outputMode"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = 0x0000, + data_type = data_types.Uint8 + }, + ["powerOnMode"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = 0x0001, + data_type = data_types.Uint8 + }, + ["ledDriveCurrent"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = 0x0002, + data_type = data_types.Uint16 + }, + ["dimTransitionTime"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = 0x0003, + data_type = data_types.Uint16 + }, + ["colorTempTransitionTime"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = 0x0004, + data_type = data_types.Uint16 + } +} + +local function device_info_changed(driver, device, event, args) + local preferences = device.preferences + local old_preferences = args.old_st_store.preferences + if preferences ~= nil then + for id, attr in pairs(preference_map) do + local old_value = old_preferences[id] + local value = preferences[id] + if value ~= nil and value ~= old_value then + value = tonumber(value) + device:send(cluster_base.write_attribute(device, + data_types.ClusterId(attr.cluster_id), + data_types.AttributeId(attr.attribute_id), + data_types.validate_or_build_type(value, attr.data_type, "payload"))) + end + end + end +end + +local firstled_light_handlers = { + NAME = "firstled-light handlers", + lifecycle_handlers = { + infoChanged = device_info_changed + }, + can_handle = require("firstled-light.can_handle") +} + +return firstled_light_handlers diff --git a/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua index b29c746d71..60390d8074 100644 --- a/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-switch/src/sub_drivers.lua @@ -37,5 +37,6 @@ return { lazy_load_if_possible("frient-IO"), lazy_load_if_possible("color_temp_range_handlers"), lazy_load_if_possible("stateless_handlers"), - lazy_load_if_possible("firstled-io") + lazy_load_if_possible("firstled-io"), + lazy_load_if_possible("firstled-light") } diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_firstled_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_firstled_light.lua new file mode 100644 index 0000000000..79b77ab12a --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_firstled_light.lua @@ -0,0 +1,106 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local PRIVATE_CLUSTER_ID = 0xFC00 + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("light-color-temp-time-restore.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "ManDai Technology", + model = "DC2DC12MiV1", + server_clusters = { 0x0006, 0x0008, 0x0300 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +-- ====================== Preferences ====================== +test.register_coroutine_test("infoChanged - outputMode 0", function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ preferences = { outputMode = "0" }})) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_attribute(mock_device, + data_types.ClusterId(PRIVATE_CLUSTER_ID), + data_types.AttributeId(0x0000), + data_types.validate_or_build_type(0, data_types.Uint8, "payload")) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - powerOnMode 0", function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ preferences = { powerOnMode = "0" }})) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_attribute(mock_device, + data_types.ClusterId(PRIVATE_CLUSTER_ID), + data_types.AttributeId(0x0001), + data_types.validate_or_build_type(0, data_types.Uint8, "payload")) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - ledDriveCurrent 100", function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ preferences = { ledDriveCurrent = "100" }})) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_attribute(mock_device, + data_types.ClusterId(PRIVATE_CLUSTER_ID), + data_types.AttributeId(0x0002), + data_types.validate_or_build_type(100, data_types.Uint16, "payload")) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - dimTransitionTime 2000", function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ preferences = { dimTransitionTime = "2000" }})) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_attribute(mock_device, + data_types.ClusterId(PRIVATE_CLUSTER_ID), + data_types.AttributeId(0x0003), + data_types.validate_or_build_type(2000, data_types.Uint16, "payload")) + }) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test("infoChanged - colorTempTransitionTime 2000", function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ preferences = { colorTempTransitionTime = "2000" }})) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_attribute(mock_device, + data_types.ClusterId(PRIVATE_CLUSTER_ID), + data_types.AttributeId(0x0004), + data_types.validate_or_build_type(2000, data_types.Uint16, "payload")) + }) + end, + { + min_api_version = 19 + } +) + +test.run_registered_tests() From b157ea084a76ae263b713325fed4ec99853d9696 Mon Sep 17 00:00:00 2001 From: Woofser Date: Thu, 2 Jul 2026 13:54:30 -0500 Subject: [PATCH 012/102] Add Sombra Shades window shade sub-driver (#2991) * add Sombra Shades driver * Add new fingerprint for Sombra Shades model * Update model identifier for Sombra Shades * Correct Sombra Shades window shade sub-driver * Use position-based motion status and add power source support * Restore WM25/L-Z and SS25/L-Z fingerprints; keep change additive * Register settle timer in position-settle tests so mock time advance fires it --- .../zigbee-window-treatment/fingerprints.yml | 5 + .../src/sombra/can_handle.lua | 14 + .../src/sombra/fingerprints.lua | 8 + .../src/sombra/init.lua | 175 +++++++ .../src/sub_drivers.lua | 1 + .../test_zigbee_window_treatment_sombra.lua | 434 ++++++++++++++++++ 6 files changed, 637 insertions(+) create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/sombra/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/sombra/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/sombra/init.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_sombra.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index b2e918746b..021d59d2d4 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -148,6 +148,11 @@ zigbeeManufacturer: manufacturer: Sombra Shades model: SS25/L-Z deviceProfileName: window-treatment-battery + - id: "Sombra Shades/SOMBRA/Z-M" + deviceLabel: Sombra Automated Shades + manufacturer: Sombra Shades + model: SOMBRA/Z-M + deviceProfileName: window-treatment-powerSource zigbeeGeneric: - id: "genericShade" diff --git a/drivers/SmartThings/zigbee-window-treatment/src/sombra/can_handle.lua b/drivers/SmartThings/zigbee-window-treatment/src/sombra/can_handle.lua new file mode 100644 index 0000000000..e407ea7198 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/sombra/can_handle.lua @@ -0,0 +1,14 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local is_zigbee_window_shade = function(opts, driver, device) + local FINGERPRINTS = require("sombra.fingerprints") + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require("sombra") + end + end + return false +end + +return is_zigbee_window_shade diff --git a/drivers/SmartThings/zigbee-window-treatment/src/sombra/fingerprints.lua b/drivers/SmartThings/zigbee-window-treatment/src/sombra/fingerprints.lua new file mode 100644 index 0000000000..bac50c21bb --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/sombra/fingerprints.lua @@ -0,0 +1,8 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "Sombra Shades", model = "SOMBRA/Z-M" } +} + +return ZIGBEE_WINDOW_SHADE_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-window-treatment/src/sombra/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/sombra/init.lua new file mode 100644 index 0000000000..634d7372e3 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/sombra/init.lua @@ -0,0 +1,175 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- require st provided libraries +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local window_shade_utils = require "window_shade_utils" +local device_management = require "st.zigbee.device_management" +local utils = require "st.utils" + +local Basic = clusters.Basic +local WindowCovering = clusters.WindowCovering +local PowerConfiguration = clusters.PowerConfiguration + +-- Sombra motors report position in the shade-industry convention +-- (0% = open, 100% = closed), which is passed straight through to +-- windowShadeLevel.shadeLevel with no inversion. +-- +-- Opening/Closing status is inferred from the direction the position is +-- changing, so it works on every Sombra motor variant -- battery and DC -- +-- regardless of any manufacturer-specific motion cluster. When position +-- reports stop arriving, a short settle timer emits the resting state. +local SETTLE_DELAY = 3 +local SETTLE_TIMER = "shade_settle_timer" + +----------------------------------------------------------------- +-- local functions +----------------------------------------------------------------- + +-- emit the resting windowShade state for a level (0 = open, 100 = closed) +local function emit_resting_state(device, level) + if level <= 0 then + device:emit_event(capabilities.windowShade.windowShade.open()) + elseif level >= 100 then + device:emit_event(capabilities.windowShade.windowShade.closed()) + else + device:emit_event(capabilities.windowShade.windowShade.partially_open()) + end +end + +-- cancel any pending settle timer +local function cancel_settle_timer(device) + local timer = device:get_field(SETTLE_TIMER) + if timer then + device.thread:cancel_timer(timer) + device:set_field(SETTLE_TIMER, nil) + end +end + +-- (re)start the settle timer; when it fires without a newer position report +-- the shade has stopped moving, so emit the resting state +local function schedule_settle(device) + cancel_settle_timer(device) + local timer = device.thread:call_with_delay(SETTLE_DELAY, function() + device:set_field(SETTLE_TIMER, nil) + local level = device:get_latest_state("main", capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + emit_resting_state(device, level) + end) + device:set_field(SETTLE_TIMER, timer) +end + +-- this is do_refresh +local do_refresh = function(self, device) + device:send(WindowCovering.attributes.CurrentPositionLiftPercentage:read(device)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) + device:send(Basic.attributes.PowerSource:read(device)) +end + +-- this is window_shade_level_cmd +local function window_shade_level_cmd(driver, device, command) + local go_to_level = command.args.shadeLevel + -- send levels without inverting as: 0% lift (open) to 100% lift (closed) + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, go_to_level)) +end + +-- this is window_shade_preset_cmd +local function window_shade_preset_cmd(driver, device, command) + local level = window_shade_utils.get_preset_level(device, command.component) + -- send levels without inverting as: 0% lift (open) to 100% lift (closed) + device:send_to_component(command.component, WindowCovering.server.commands.GoToLiftPercentage(device, level)) +end + +-- this is device_added +local function device_added(self, device) + device:emit_event(capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" }, {visibility = {displayed = false}})) + device.thread:call_with_delay(3, function(d) + do_refresh(self, device) + end) +end + +-- this is current_position_attr_handler +-- position is 0 = open, 100 = closed; direction is inferred from the change +local function current_position_attr_handler(driver, device, value, zb_rx) + local level = value.value + local previous = device:get_latest_state("main", capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + local windowShade = capabilities.windowShade.windowShade + + device:emit_event(capabilities.windowShadeLevel.shadeLevel(level)) + + if level <= 0 then + cancel_settle_timer(device) + device:emit_event(windowShade.open()) + elseif level >= 100 then + cancel_settle_timer(device) + device:emit_event(windowShade.closed()) + else + if level < previous then + device:emit_event(windowShade.opening()) + elseif level > previous then + device:emit_event(windowShade.closing()) + end + schedule_settle(device) + end +end + +-- this is do_configure +local function do_configure(self, device) + -- configure elements + device:send(device_management.build_bind_request(device, WindowCovering.ID, self.environment_info.hub_zigbee_eui)) + device:send(WindowCovering.attributes.CurrentPositionLiftPercentage:configure_reporting(device, 1, 3600, 1)) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 1, 3600, 1)) + device:send(device_management.build_bind_request(device, Basic.ID, self.environment_info.hub_zigbee_eui)) + device:send(Basic.attributes.PowerSource:configure_reporting(device, 1, 3600)) + + -- read elements + device.thread:call_with_delay(3, function(d) + do_refresh(self, device) + end) +end + +-- this is battery_perc_attr_handler +local function battery_perc_attr_handler(driver, device, value, zb_rx) + local converted_value = value.value / 2 + converted_value = utils.round(converted_value) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, + capabilities.battery.battery(utils.clamp_value(converted_value, 0, 100))) +end + +-- create the handler object +local sombra_roller_shade_handler = { + NAME = "sombra_roller_shade_handler", + lifecycle_handlers = { + added = device_added, + doConfigure = do_configure + }, + capability_handlers = { + [capabilities.windowShadeLevel.ID] = { + [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level_cmd + }, + [capabilities.windowShadePreset.ID] = { + [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset_cmd + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + zigbee_handlers = { + attr = { + [WindowCovering.ID] = { + [WindowCovering.attributes.CurrentPositionLiftPercentage.ID] = current_position_attr_handler, + }, + [PowerConfiguration.ID] = { + [PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_perc_attr_handler, + } + } + }, + can_handle = require("sombra.can_handle"), +} + +-- return the handler +return sombra_roller_shade_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua index 959c8d8c22..3353c87fd4 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/sub_drivers.lua @@ -7,6 +7,7 @@ local sub_drivers = { lazy_load_if_possible("aqara"), lazy_load_if_possible("feibit"), lazy_load_if_possible("somfy"), + lazy_load_if_possible("sombra"), lazy_load_if_possible("invert-lift-percentage"), lazy_load_if_possible("rooms-beautiful"), lazy_load_if_possible("axis"), diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_sombra.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_sombra.lua new file mode 100644 index 0000000000..35ab9eab24 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_sombra.lua @@ -0,0 +1,434 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" + +local Basic = clusters.Basic +local WindowCovering = clusters.WindowCovering +local PowerConfiguration = clusters.PowerConfiguration + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("window-treatment-powerSource.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Sombra Shades", + model = "SOMBRA/Z-M", + server_clusters = {0x0000, 0x0001, 0x0102} + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Window Shade state open", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Window Shade state closed", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "A falling position reports opening, then settles to partially open", + function() + test.socket.capability:__set_channel_ordering("relaxed") + -- establish a known starting position (fully closed) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 100) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) + ) + test.wait_for_events() + -- a lower position than before => opening + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 60) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(60)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) + ) + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.wait_for_events() + -- no further reports for the settle delay => partially open + test.mock_time.advance_time(3) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "A rising position reports closing, then settles to partially open", + function() + test.socket.capability:__set_channel_ordering("relaxed") + -- establish a known starting position (fully open) + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(0)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) + ) + test.wait_for_events() + -- a higher position than before => closing + test.socket.zigbee:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 40) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(40)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) + ) + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.wait_for_events() + -- no further reports for the settle delay => partially open + test.mock_time.advance_time(3) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowShade open cmd test case", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "open", args = {} } + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.UpOrOpen(mock_device) + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowShade close cmd test case", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "close", args = {} } + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.DownOrClose(mock_device) + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowShade pause cmd test case", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShade", component = "main", command = "pause", args = {} } + } + ) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.server.commands.Stop(mock_device) + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Set shade level command", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 45 } } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 45) + } + ) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Preset position command", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { capability = "windowShadePreset", component = "main", command = "presetPosition", args = {} } + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + WindowCovering.server.commands.GoToLiftPercentage(mock_device, 50) + } + ) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Battery Percentage Remaining test cases", + function() + local battery_test_map = { + [200] = 100, + [100] = 50, + [0] = 0 + } + for bat_perc_rem, batt_perc_out in pairs(battery_test_map) do + test.socket.zigbee:__queue_receive({ mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, bat_perc_rem) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(batt_perc_out)) ) + test.wait_for_events() + end + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Power Source test cases", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.zigbee:__queue_receive({ mock_device.id, + Basic.attributes.PowerSource:build_test_attr_report(mock_device, 3) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery())) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, + Basic.attributes.PowerSource:build_test_attr_report(mock_device, 4) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerSource.powerSource.dc())) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, + Basic.attributes.PowerSource:build_test_attr_report(mock_device, 0) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.powerSource.powerSource.unknown())) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Refresh should generate expected messages", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } }) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Basic.attributes.PowerSource:read(mock_device) + }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "doConfigure should generate expected messages", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zigbee:__set_channel_ordering("relaxed") + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, WindowCovering.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:configure_reporting(mock_device, 1, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 1, 3600, 1) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Basic.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Basic.attributes.PowerSource:configure_reporting(mock_device, 1, 3600) + }) + + -- read values after delay + test.mock_time.advance_time(3) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Basic.attributes.PowerSource:read(mock_device) + }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "added should generate expected messages", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added"}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.windowShade.supportedWindowShadeCommands({ "open", "close", "pause" },{ visibility = { displayed = false }})) + ) + test.timer.__create_and_queue_test_time_advance_timer(3, "oneshot") + test.wait_for_events() + test.socket.zigbee:__set_channel_ordering("relaxed") + + -- read values after delay + test.mock_time.advance_time(3) + test.socket.zigbee:__expect_send({ + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Basic.attributes.PowerSource:read(mock_device) + }) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() From 18f37510551fa787e540f1cf27d035d2d45b0b98 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 2 Jul 2026 13:56:56 -0500 Subject: [PATCH 013/102] WWSTCERT-12453 Aqara Smart Lock J200 (#3074) --- drivers/SmartThings/matter-lock/fingerprints.yml | 5 +++++ .../matter-lock/src/new-matter-lock/fingerprints.lua | 1 + 2 files changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 796cefd590..01e662c421 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -25,6 +25,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x2804 deviceProfileName: lock + - id: "4447/10253" + deviceLabel: Aqara Smart Lock J200 + vendorId: 0x115F + productId: 0x280D + deviceProfileName: lock-modular #Eufy - id: "5427/1" deviceLabel: eufy Smart Lock E31 diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua index 375b8c6189..d3127d4b8a 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua @@ -11,6 +11,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x280e}, -- AQARA Smart Gate Lock U500 {0x115f, 0x280f}, -- AQARA Smart Rim Lock U500 {0x115f, 0x2810}, -- AQARA Smart Glass Door Lock U500 + {0x115F, 0x280D}, -- Aqara Smart Lock J200 {0x147F, 0x0001}, -- U-tec {0x147F, 0x0007}, -- ULTRALOQ Bolt Pro Smart Matter Door Lock {0x147F, 0x0008}, -- Ultraloq, Bolt Smart Matter Door Lock From 4a65dd403106269ae8286f0e1b80cc3e3c98cd7b Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 2 Jul 2026 14:04:08 -0500 Subject: [PATCH 014/102] WWSTCERT-12567 Aqara Smart Lock U500 Rim (#3086) --- drivers/SmartThings/matter-lock/fingerprints.yml | 14 ++++++++++++++ .../src/new-matter-lock/fingerprints.lua | 5 +++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 01e662c421..575da17382 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -25,6 +25,20 @@ matterManufacturer: vendorId: 0x115F productId: 0x2804 deviceProfileName: lock + - id: "4447/10255" + deviceLabel: Aqara Smart Lock U500 Rim + vendorId: 0x115F + productId: 0x280F + deviceProfileName: lock-modular + - id: "4447/10256" + deviceLabel: Aqara Smart Lock U500 Glass Door + vendorId: 0x115F + productId: 0x2810 + deviceProfileName: lock-modular + - id: "4447/10314" + deviceLabel: Aqara Smart Lock U600 + vendorId: 0x115F + productId: 0x284A - id: "4447/10253" deviceLabel: Aqara Smart Lock J200 vendorId: 0x115F diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua index d3127d4b8a..4b15b23b2b 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua @@ -9,8 +9,9 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x115f, 0x286A}, -- AQARA, U200 US {0x115f, 0x2805}, -- Aqara Smart Lock J200 Set {0x115f, 0x280e}, -- AQARA Smart Gate Lock U500 - {0x115f, 0x280f}, -- AQARA Smart Rim Lock U500 - {0x115f, 0x2810}, -- AQARA Smart Glass Door Lock U500 + {0x115F, 0x280F}, -- Aqara Smart Lock U500 Rim + {0x115F, 0x2810}, -- Aqara Smart Lock U500 Glass Door + {0x115F, 0x284A}, -- Aqara Smart Lock U600 {0x115F, 0x280D}, -- Aqara Smart Lock J200 {0x147F, 0x0001}, -- U-tec {0x147F, 0x0007}, -- ULTRALOQ Bolt Pro Smart Matter Door Lock From 59aea94ac9af614d186c5d3d80a0069b71cb3616 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Fri, 3 Jul 2026 03:13:21 +0800 Subject: [PATCH 015/102] add wallhero_thermostat_ACL-403STC1 (#2969) --- .../zigbee-thermostat/fingerprints.yml | 5 + .../thermostat-thirty-buttons-wallhero.yml | 249 ++++ .../zigbee-thermostat/src/init.lua | 4 +- .../zigbee-thermostat/src/sub_drivers.lua | 1 + .../test_zigbee_thermostat_wallhero_3in1.lua | 1240 +++++++++++++++++ .../src/wallhero/can_handle.lua | 13 + .../src/wallhero/fingerprints.lua | 6 + .../zigbee-thermostat/src/wallhero/init.lua | 232 +++ tools/localizations/cn.csv | 1 + 9 files changed, 1750 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-thermostat/profiles/thermostat-thirty-buttons-wallhero.yml create mode 100644 drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/wallhero/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/wallhero/fingerprints.lua create mode 100644 drivers/SmartThings/zigbee-thermostat/src/wallhero/init.lua diff --git a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml index d88b3ff89a..efa89be552 100644 --- a/drivers/SmartThings/zigbee-thermostat/fingerprints.yml +++ b/drivers/SmartThings/zigbee-thermostat/fingerprints.yml @@ -134,6 +134,11 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.airrtc.agl001 deviceProfileName: thermostat-aqara + - id: "WALL HERO/ACL-403STC1" + deviceLabel: WALLHERO Thermostat ACL-403STC1 + manufacturer: WALL HERO + model: ACL-403STC1 + deviceProfileName: thermostat-thirty-buttons-wallhero zigbeeGeneric: - id: "genericThermostat" deviceLabel: Zigbee Thermostat diff --git a/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-thirty-buttons-wallhero.yml b/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-thirty-buttons-wallhero.yml new file mode 100644 index 0000000000..5a7bff92a8 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/profiles/thermostat-thirty-buttons-wallhero.yml @@ -0,0 +1,249 @@ +name: thermostat-thirty-buttons-wallhero +components: + - id: main + label: 空调(AC) + capabilities: + - id: switch + version: 1 + - id: temperatureMeasurement + version: 1 + - id: temperatureSetpoint + version: 1 + - id: thermostatMode + version: 1 + - id: fanMode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: RemoteController + - id: fan + label: 新风(Fresh Air) + capabilities: + - id: switch + version: 1 + - id: fanMode + version: 1 + categories: + - name: Fan + - id: heat + label: 地暖(Floor Heating) + capabilities: + - id: switch + version: 1 + - id: thermostatHeatingSetpoint + version: 1 + categories: + - name: Thermostat + - id: button1 + label: 回家(Home) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + label: 离家(Leave Home) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + label: 晨起(Morning) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button4 + label: 睡眠(Sleep) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button5 + label: 阅读(Reading) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button6 + label: 观影(Movie) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button7 + label: 全开(All On) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button8 + label: 全关(All Off) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button9 + label: 空调温度调高(AC Temp Up) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button10 + label: 空调温度降低(AC Temp Down) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button11 + label: 开窗帘(Open Curtain) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button12 + label: 关窗帘(Close Curtain) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button13 + label: 空调开(AC On) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button14 + label: 空调关(AC Off) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button15 + label: 无风开(No Wind On) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button16 + label: 无风关(No Wind Off) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button17 + label: 制冷(Cooling) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button18 + label: 制热(Heating) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button19 + label: 新风开(Fresh Air On) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button20 + label: 新风关(Fresh Air Off) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button21 + label: 开电视(Turn On TV) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button22 + label: 关电视(Turn Off TV) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button23 + label: 开音乐(Play Music) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button24 + label: 关音乐(Stop Music) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button25 + label: 开纱帘(Open Screen Curtain) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button26 + label: 关纱帘(Close Screen Curtain) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button27 + label: 洗衣服(Wash Clothes) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button28 + label: 衣服护理(Clothes Care) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button29 + label: 用餐(Dining) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button30 + label: 开大门(Open Main Door) + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController diff --git a/drivers/SmartThings/zigbee-thermostat/src/init.lua b/drivers/SmartThings/zigbee-thermostat/src/init.lua index a72b3c9107..cfbbd76a0c 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/init.lua @@ -41,7 +41,9 @@ local THERMOSTAT_MODE_MAP = { [ThermostatSystemMode.AUTO] = ThermostatMode.thermostatMode.auto, [ThermostatSystemMode.COOL] = ThermostatMode.thermostatMode.cool, [ThermostatSystemMode.HEAT] = ThermostatMode.thermostatMode.heat, - [ThermostatSystemMode.EMERGENCY_HEATING] = ThermostatMode.thermostatMode.emergency_heat + [ThermostatSystemMode.EMERGENCY_HEATING] = ThermostatMode.thermostatMode.emergency_heat, + [ThermostatSystemMode.DRY] = ThermostatMode.thermostatMode.dryair, + [ThermostatSystemMode.FAN_ONLY] = ThermostatMode.thermostatMode.fanonly } local FAN_MODE_MAP = { diff --git a/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua b/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua index 7f62589c24..3b535c3cf0 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/sub_drivers.lua @@ -15,5 +15,6 @@ local sub_drivers = { lazy_load_if_possible("vimar"), lazy_load_if_possible("resideo_korea"), lazy_load_if_possible("aqara"), + lazy_load_if_possible("wallhero"), } return sub_drivers diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua new file mode 100644 index 0000000000..a93a3a36ee --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua @@ -0,0 +1,1240 @@ +-- Copyright 2026 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local Scenes = clusters.Scenes +local OnOff = clusters.OnOff +local FanControl = clusters.FanControl +local Thermostat = clusters.Thermostat +local ThermostatMode = capabilities.thermostatMode +local FanMode = capabilities.fanMode +local button_attr = capabilities.button.button + +local SUPPORTED_FAN_MODES = { + { "auto", "high", "medium", "low"}, +} + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("thermostat-thirty-buttons-wallhero.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "WALL HERO", + model = "ACL-403STC1", + server_clusters = { 0x0005,0x0006,0x0201,0x0202, 0x0203 } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "added lifecycle event", + function() + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + ThermostatMode.supportedThermostatModes({"cool", "dryair", "fanonly", "heat"}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + FanMode.supportedFanModes( SUPPORTED_FAN_MODES[1] , { visibility = { displayed = false }}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "fan", + FanMode.supportedFanModes( SUPPORTED_FAN_MODES[1] , { visibility = { displayed = false }}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.temperatureSetpoint.temperatureSetpointRange({ value = { minimum = 16.00, maximum = 32.00 }, unit = "C" }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "heat", + capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 16.00, maximum = 32.00 }, unit = "C" }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.switch.switch.off() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "heat", + capabilities.switch.switch.off() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "fan", + capabilities.switch.switch.off() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + ThermostatMode.thermostatMode.cool() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + FanMode.fanMode.auto() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "fan", + FanMode.fanMode.auto() + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.temperatureSetpoint.temperatureSetpoint({value = 26, unit = "C"}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "heat", + capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 26, unit = "C"}) + ) + ) + + for _, component in pairs(mock_device.profile.components) do + if component.id ~= "main" and component.id ~= "heat" and component.id ~= "fan" then + test.socket.capability:__expect_send( + mock_device:generate_test_message( + component.id, + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + component.id, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }) + ) + ) + end + end + + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device):to_endpoint(0x01) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.LocalTemperature:read(mock_device):to_endpoint(0x01) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:read(mock_device):to_endpoint(0x01) + } + ) + + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.SystemMode:read(mock_device):to_endpoint(0x01) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + FanControl.attributes.FanMode:read(mock_device):to_endpoint(0x01) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device):to_endpoint(0x02) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + FanControl.attributes.FanMode:read(mock_device):to_endpoint(0x02) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device):to_endpoint(0x03) + } + ) + test.socket.zigbee:__expect_send( + { + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:read(mock_device):to_endpoint(0x03) + } + ) + test.socket:set_time_advance_per_select(0.1) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + + +test.register_message_test( + "Reported on status should be handled: on ep 1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + true):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.on()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Reported off status should be handled: off ep 1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + false):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.switch.switch.off()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Temperature reports using the temperatureMeasurement should be handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.LocalTemperature:build_test_attr_report(mock_device, + 2500):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 25.0, unit = "C" })) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Cooling setpoint reports temperatureSetpoint are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:build_test_attr_report(mock_device, + 2500):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureSetpoint.temperatureSetpoint({ value = 25.0, unit = "C" })) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Thermostat running mode reports 3 cool are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.ThermostatRunningMode:build_test_attr_report(mock_device, + 3):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode("cool")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Thermostat running mode reports 4 heat are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.ThermostatRunningMode:build_test_attr_report(mock_device, + 4):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode("heat")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Thermostat running mode reports 8 dryair are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.ThermostatRunningMode:build_test_attr_report(mock_device, + 8):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode("dryair")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Thermostat running mode reports 1 auto are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.ThermostatRunningMode:build_test_attr_report(mock_device, + 1):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode("auto")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "FanControl fan mode reports 1 low are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 1):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("low")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "FanControl fan mode reports 2 medium are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 2):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("medium")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "FanControl fan mode reports 3 high are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 3):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("high")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "FanControl fan mode reports 5 auto are handled : ep1", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 5):from_endpoint(0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.fanMode.fanMode("auto")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Reported on status should be handled: on ep 3", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + true):from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("heat", capabilities.switch.switch.on()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Reported off status should be handled: off ep 3", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + false):from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("heat", capabilities.switch.switch.off()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Heating setpoint reports heatingSetpoint are handled : ep3", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:build_test_attr_report(mock_device, + 2500):from_endpoint(0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("heat", capabilities.thermostatHeatingSetpoint.heatingSetpoint({ value = 25.0, unit = "C" })) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Reported on status should be handled: on ep 2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + true):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.switch.switch.on()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Reported off status should be handled: off ep 2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + false):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.switch.switch.off()) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "FanControl fan mode reports are handled : ep2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 1):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.fanMode.fanMode("low")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "FanControl fan mode reports are handled : ep2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 2):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.fanMode.fanMode("medium")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "FanControl fan mode reports are handled : ep2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 3):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.fanMode.fanMode("high")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "FanControl fan mode reports 5 auto are handled : ep2", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, FanControl.attributes.FanMode:build_test_attr_report(mock_device, + 5):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("fan", capabilities.fanMode.fanMode("auto")) + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability on command switch on should be handled : ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.On(mock_device):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability off command switch off should be handled : ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "main", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.Off(mock_device):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability on command switch on should be handled : ep3", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "heat", command = "on", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.On(mock_device):to_endpoint(0x03) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability off command switch off should be handled : ep3", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "heat", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.Off(mock_device):to_endpoint(0x03) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability on command switch on should be handled : ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "fan", command = "on", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.On(mock_device):to_endpoint(0x02) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability off command switch off should be handled : ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "switch", component = "fan", command = "off", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.Off(mock_device):to_endpoint(0x02) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability temperatureSetpoint command setpoint 27 ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "temperatureSetpoint", component = "main", command = "setTemperatureSetpoint", args = { 27 } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 2700):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability thermostat command cool ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"cool" } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 3):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability thermostat command dryair ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"dryair" } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 8):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability thermostat command fanonly ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = {"fanonly" } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 7):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability thermostat command heat ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatMode", component = "main", command = "setThermostatMode", args = { "heat"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 4):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability fanMode command auto ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "main", command = "setFanMode", args = {"auto"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 5):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability fanMode command low ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "main", command = "setFanMode", args = {"low"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 1):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability fanMode command medium ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "main", command = "setFanMode", args = {"medium"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 2):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability fanMode command high ep1", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "main", command = "setFanMode" , args = {"high"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 3):to_endpoint(0x01) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability thermostatHeatingSetpoint command setHeatingSetpoint 27 ep3", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "thermostatHeatingSetpoint", component = "heat", command = "setHeatingSetpoint", args = { 27 } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 2700):to_endpoint(0x03) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability fanMode command auto ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "fan", command = "setFanMode", args = {"auto"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 5):to_endpoint(0x02) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability fanMode command low ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "fan", command = "setFanMode", args = {"low"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 1):to_endpoint(0x02) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability fanMode command medium ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "fan", command = "setFanMode", args = {"medium"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 2):to_endpoint(0x02) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Capability fanMode command high ep2", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "fanMode", component = "fan", command = "setFanMode" , args = {"high"} } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 3):to_endpoint(0x02) } + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "RecallScene command should be handled", + function() + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 4) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button1", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 5) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button2", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 6) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button3", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 7) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button4", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 8) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button5", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 9) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button6", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 10) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button7", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 11) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button8", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 12) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button9", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 13) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button10", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 14) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button11", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 15) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button12", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 16) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button13", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 17) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button14", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 18) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button15", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 19) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button16", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 20) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button17", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 21) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button18", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 22) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button19", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 23) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button20", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 24) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button21", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 25) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button22", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 26) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button23", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 27) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button24", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 28) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button25", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 29) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button26", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 30) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button27", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 31) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button28", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 32) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button29", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 33) }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("button30", button_attr.pushed({ state_change = true })) + ) + + test.wait_for_events() + test.socket.zigbee:__queue_receive({ mock_device.id, zigbee_test_utils.build_custom_command_id(mock_device, Scenes.ID, Scenes.server.commands.RecallScene.ID, 0x0000, "\x05\x00\x00\x00\x05\x00", 34) }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/wallhero/can_handle.lua b/drivers/SmartThings/zigbee-thermostat/src/wallhero/can_handle.lua new file mode 100644 index 0000000000..476deedf2b --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/wallhero/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 FINGERPRINTS = require "wallhero.fingerprints" + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("wallhero") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-thermostat/src/wallhero/fingerprints.lua b/drivers/SmartThings/zigbee-thermostat/src/wallhero/fingerprints.lua new file mode 100644 index 0000000000..d1a79015f1 --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/wallhero/fingerprints.lua @@ -0,0 +1,6 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return { + { mfr = "WALL HERO", model = "ACL-403STC1" } +} diff --git a/drivers/SmartThings/zigbee-thermostat/src/wallhero/init.lua b/drivers/SmartThings/zigbee-thermostat/src/wallhero/init.lua new file mode 100644 index 0000000000..807f2e564b --- /dev/null +++ b/drivers/SmartThings/zigbee-thermostat/src/wallhero/init.lua @@ -0,0 +1,232 @@ +-- Copyright 2026 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +local log = require "log" +local socket = require "cosock.socket" +local zcl_clusters = require "st.zigbee.zcl.clusters" + +local Scenes = zcl_clusters.Scenes +local OnOff = zcl_clusters.OnOff +local FanControl = zcl_clusters.FanControl +local Thermostat = zcl_clusters.Thermostat +local ThermostatMode = capabilities.thermostatMode +local FanMode = capabilities.fanMode + +local function scenes_cluster_handler(driver, device, zb_rx) + local additional_fields = { + state_change = true + } + --Endpoint 1 to 3 are respectively the air conditioner, fresh air, and floor heating. Endpoint 4 to Endpoint 33 are buttons. + local ep = zb_rx.address_header.src_endpoint.value - 3 + local button_name = "button" .. ep + local event = capabilities.button.button.pushed(additional_fields) + local comp = device.profile.components[button_name] + if comp ~= nil then + device:emit_component_event(comp, event) + else + log.warn("Attempted to emit button event for unknown button: " .. button_name) + end +end + +local function on_off_attr_handler(driver, device, value, zb_rx) + local attr = capabilities.switch.switch + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, value.value and attr.on() or attr.off()) +end + +local function on_handler(driver, device, command) + device:send_to_component(command.component, OnOff.server.commands.On(device)) +end + +local function off_handler(driver, device, command) + device:send_to_component(command.component, OnOff.server.commands.Off(device)) +end + +local function caps_temperatureSetpoint_handler(driver, device, command) + local temperature = tonumber(command.args.setpoint)*100 + device:send_to_component(command.component, Thermostat.attributes.OccupiedCoolingSetpoint:write(device,temperature)) +end + +local SUPPORTED_FAN_MODES = { + { "auto", "high", "medium", "low"}, +} + +local FAN_MODE_TO_ZIGBEE = { + ["auto"] = 0x05, + ["low"] = 0x01, + ["medium"] = 0x02, + ["high"] = 0x03 +} + +local ZIGBEE_TO_FAN_MODES = { + [1] = "low" , + [2] = "medium" , + [3] = "high" , + [5] = "auto" +} + +local function fan_mode_attr_handler(driver, device, value, zb_rx) + local ep = zb_rx.address_header.src_endpoint.value + local str = ZIGBEE_TO_FAN_MODES[value.value] + if ep == 1 then + device:emit_component_event(device.profile.components.main,FanMode.fanMode({ value = str })) + elseif ep == 2 then + device:emit_component_event(device.profile.components.fan,FanMode.fanMode({ value = str })) + end +end + +local function setFanMode_handler(driver, device, command) + local value = FAN_MODE_TO_ZIGBEE[command.args.fanMode] + device:send_to_component(command.component, FanControl.attributes.FanMode:write(device, value)) +end + +local function thermostat_attr_occupiedCoolingSetpoint_handler(driver, device, value, zb_rx) + local ep = zb_rx.address_header.src_endpoint.value + if ep == 1 then + local temp = value.value/100 + device:emit_component_event(device.profile.components.main,capabilities.temperatureSetpoint.temperatureSetpoint({value = temp, unit = "C"})) + end +end + +local function do_refresh(driver, device) + device:send_to_component("main", OnOff.attributes.OnOff:read(device)) + device:send_to_component("main", Thermostat.attributes.LocalTemperature:read(device)) + device:send_to_component("main", Thermostat.attributes.OccupiedCoolingSetpoint:read(device)) + + device:send_to_component("main", Thermostat.attributes.SystemMode:read(device)) + device:send_to_component("main", FanControl.attributes.FanMode:read(device)) + device:send_to_component("fan", OnOff.attributes.OnOff:read(device)) + + device:send_to_component("fan", FanControl.attributes.FanMode:read(device)) + device:send_to_component("heat", OnOff.attributes.OnOff:read(device)) + device:send_to_component("heat", Thermostat.attributes.OccupiedHeatingSetpoint:read(device)) +end + +local function added_handler(driver, device) + device:emit_component_event(device.profile.components.main,ThermostatMode.supportedThermostatModes({"cool", "dryair", "fanonly", "heat"}, { visibility = { displayed = false } })) + device:emit_component_event(device.profile.components.main,FanMode.supportedFanModes( SUPPORTED_FAN_MODES[1] , { visibility = { displayed = false }})) + device:emit_component_event(device.profile.components.fan,FanMode.supportedFanModes( SUPPORTED_FAN_MODES[1] , { visibility = { displayed = false }})) + + device:emit_component_event(device.profile.components.main,capabilities.temperatureSetpoint.temperatureSetpointRange({ value = { minimum = 16.00, maximum = 32.00 }, unit = "C" })) + device:emit_component_event(device.profile.components.heat,capabilities.thermostatHeatingSetpoint.heatingSetpointRange({ value = { minimum = 16.00, maximum = 32.00 }, unit = "C" })) + + device:emit_component_event(device.profile.components.main,capabilities.switch.switch.off()) + device:emit_component_event(device.profile.components.heat,capabilities.switch.switch.off()) + device:emit_component_event(device.profile.components.fan,capabilities.switch.switch.off()) + + device:emit_component_event(device.profile.components.main,ThermostatMode.thermostatMode.cool()) + device:emit_component_event(device.profile.components.main,FanMode.fanMode.auto()) + device:emit_component_event(device.profile.components.fan,FanMode.fanMode.auto()) + device:emit_component_event(device.profile.components.main,capabilities.temperatureSetpoint.temperatureSetpoint({value = 26, unit = "C"})) + device:emit_component_event(device.profile.components.heat,capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 26, unit = "C"})) + + for _, component in pairs(device.profile.components) do + if component.id ~= "main" and component.id ~= "heat" and component.id ~= "fan" then + device:emit_component_event(component, + capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } })) + device:emit_component_event(component, + capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } })) + -- Without this time delay, the state of some buttons cannot be updated + socket.sleep(1) + end + end + do_refresh(driver, device) +end + +local function component_to_endpoint(device, component_id) + local ep_num + if component_id == "main" then + ep_num = 1 + elseif component_id == "fan" then + ep_num = 2 + elseif component_id == "heat" then + ep_num = 3 + end + return ep_num or device.fingerprinted_endpoint_id +end + +local function endpoint_to_component(device, ep) + if ep > 3 then + ep = ep - 3 + local button_comp = string.format("button%d", ep) + if device.profile.components[button_comp] ~= nil then + return button_comp + else + return nil + end + else + if ep == 1 then + return "main" + elseif ep == 2 then + return "fan" + else + return "heat" + end + end +end + +local device_init = function(self, device) + device:set_component_to_endpoint_fn(component_to_endpoint) + device:set_endpoint_to_component_fn(endpoint_to_component) +end + +local wallhero_thermostat_3in1 = { + NAME = "Wall Hero thermostat 3in1", + supported_capabilities = { + capabilities.switch, + capabilities.temperatureSetpoint + }, + lifecycle_handlers = { + init = device_init, + added = added_handler + }, + health_check = false, + zigbee_handlers = { + cluster = { + [Scenes.ID] = { + [Scenes.server.commands.RecallScene.ID] = scenes_cluster_handler, + } + }, + attr = { + [OnOff.ID] = { + [OnOff.attributes.OnOff.ID] = on_off_attr_handler + }, + [FanControl.ID] = { + [FanControl.attributes.FanMode.ID] = fan_mode_attr_handler + }, + [Thermostat.ID] = { + [Thermostat.attributes.OccupiedCoolingSetpoint.ID] = thermostat_attr_occupiedCoolingSetpoint_handler + } + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = on_handler, + [capabilities.switch.commands.off.NAME] = off_handler + }, + [capabilities.temperatureSetpoint.ID] = { + [capabilities.temperatureSetpoint.commands.setTemperatureSetpoint.NAME] = caps_temperatureSetpoint_handler + }, + [capabilities.fanMode.ID] = { + [capabilities.fanMode.commands.setFanMode.NAME] = setFanMode_handler + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + } + }, + + can_handle = require("wallhero.can_handle") +} + +return wallhero_thermostat_3in1 diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index d1a3ed3369..007f8059b5 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -141,3 +141,4 @@ 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 +"WALLHERO Thermostat ACL-403STC1",墙壁侠温控ACL-403STC1 From 79b44011cd988d951032a12469d5ee97f82ce746 Mon Sep 17 00:00:00 2001 From: Konrad K <33450498+KKlimczukS@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:55:09 +0200 Subject: [PATCH 016/102] fingerprint for Sonoff SNZB-02B (#3079) --- drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml index d897ca7ecf..ee36659670 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml +++ b/drivers/SmartThings/zigbee-humidity-sensor/fingerprints.yml @@ -88,6 +88,11 @@ zigbeeManufacturer: manufacturer: SONOFF model: SNZB-02DR2 deviceProfileName: humidity-temp-battery + - id: "SONOFF/SNZB-02B" + deviceLabel: "SONOFF SNZB-02B" + manufacturer: SONOFF + model: SNZB-02B + deviceProfileName: humidity-temp-battery - id: "Third Reality/3RTHS24BZ" deviceLabel: ThirdReality Temperature and Humidity Sensor manufacturer: Third Reality, Inc From f1303a2014943278cf54d676a8c84f8d9c814538 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:50:04 -0500 Subject: [PATCH 017/102] Matter Window Covering: Add embedded clusters for Descriptor, Global clusters (#3088) --- .../ClosureControl/server/commands/MoveTo.lua | 2 +- .../types/OverallCurrentStateStruct.lua | 2 +- .../types/OverallTargetStateStruct.lua | 2 +- .../server/commands/SetTarget.lua | 2 +- .../ClosureDimension/server/commands/Step.lua | 2 +- .../types/DimensionStateStruct.lua | 2 +- .../src/embedded_clusters/Descriptor/init.lua | 55 +++++++++++ .../server/attributes/PartsList.lua | 78 +++++++++++++++ .../Descriptor/server/attributes/TagList.lua | 80 ++++++++++++++++ .../Descriptor/server/attributes/init.lua | 27 ++++++ .../Descriptor/types/Feature.lua | 56 +++++++++++ .../Descriptor/types/init.lua | 9 ++ .../src/embedded_clusters/Global/init.lua | 8 ++ .../Global/types/SemanticTagStruct.lua | 94 +++++++++++++++++++ .../Global/types/ThreeLevelAutoEnum.lua | 38 ++++++++ .../embedded_clusters/Global/types/init.lua | 18 ++++ .../src/sub_drivers/closure/can_handle.lua | 3 +- .../closure_handlers/attribute_handlers.lua | 4 + .../closure_utils/embedded_cluster_utils.lua | 62 ++++++++++++ .../closure/closure_utils/utils.lua | 5 +- .../src/sub_drivers/closure/init.lua | 9 +- .../init.lua | 2 +- .../src/test/test_matter_closure.lua | 5 + .../src/test/test_matter_window_covering.lua | 5 + 24 files changed, 559 insertions(+), 11 deletions(-) create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/init.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/TagList.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/init.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/types/Feature.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/types/init.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/init.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/SemanticTagStruct.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/ThreeLevelAutoEnum.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/init.lua create mode 100644 drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/embedded_cluster_utils.lua diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua index 80c1bca52d..fb83ed49c2 100644 --- a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/server/commands/MoveTo.lua @@ -25,7 +25,7 @@ MoveTo.field_defs = { field_id = 2, is_nullable = false, is_optional = true, - data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + data_type = require "embedded_clusters.Global.types.ThreeLevelAutoEnum", }, } diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua index 41af9d66db..cf4aaa91df 100644 --- a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallCurrentStateStruct.lua @@ -24,7 +24,7 @@ OverallCurrentStateStruct.field_defs = { field_id = 2, is_nullable = false, is_optional = true, - data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + data_type = require "embedded_clusters.Global.types.ThreeLevelAutoEnum", }, { name = "secure_state", diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua index eac6492815..f52fff4d1c 100644 --- a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureControl/types/OverallTargetStateStruct.lua @@ -24,7 +24,7 @@ OverallTargetStateStruct.field_defs = { field_id = 2, is_nullable = false, is_optional = true, - data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + data_type = require "embedded_clusters.Global.types.ThreeLevelAutoEnum", }, } diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua index d2f4a51b78..c14a550f16 100644 --- a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/SetTarget.lua @@ -25,7 +25,7 @@ SetTarget.field_defs = { field_id = 2, is_nullable = false, is_optional = true, - data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + data_type = require "embedded_clusters.Global.types.ThreeLevelAutoEnum", }, } diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua index ce70c7e680..597d2ddd74 100644 --- a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/server/commands/Step.lua @@ -25,7 +25,7 @@ Step.field_defs = { field_id = 2, is_nullable = false, is_optional = true, - data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + data_type = require "embedded_clusters.Global.types.ThreeLevelAutoEnum", }, } diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua index 56958b2020..ecf718cbf3 100644 --- a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/ClosureDimension/types/DimensionStateStruct.lua @@ -24,7 +24,7 @@ DimensionStateStruct.field_defs = { field_id = 2, is_nullable = false, is_optional = true, - data_type = require "st.matter.generated.zap_clusters.Global.types.ThreeLevelAutoEnum", + data_type = require "embedded_clusters.Global.types.ThreeLevelAutoEnum", }, } diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/init.lua new file mode 100644 index 0000000000..78739a10db --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/init.lua @@ -0,0 +1,55 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local cluster_base = require "st.matter.cluster_base" +local DescriptorServerAttributes = require "embedded_clusters.Descriptor.server.attributes" + +local Descriptor = {} + +Descriptor.ID = 0x001D +Descriptor.NAME = "Descriptor" +Descriptor.server = {} +Descriptor.client = {} +Descriptor.server.attributes = DescriptorServerAttributes:set_parent_cluster(Descriptor) + +function Descriptor:get_attribute_by_id(attr_id) + local attr_id_map = { + [0x0003] = "PartsList", + [0x0004] = "TagList", + } + local attr_name = attr_id_map[attr_id] + if attr_name ~= nil then + return self.attributes[attr_name] + end + return nil +end + +function Descriptor:get_server_command_by_id(command_id) + local server_id_map = { + } + if server_id_map[command_id] ~= nil then + return self.server.commands[server_id_map[command_id]] + end + return nil +end + +Descriptor.attribute_direction_map = { + ["PartsList"] = "server", + ["TagList"] = "server", +} + +local attribute_helper_mt = {} +attribute_helper_mt.__index = function(self, key) + local direction = Descriptor.attribute_direction_map[key] + if direction == nil then + error(string.format("Referenced unknown attribute %s on cluster %s", key, Descriptor.NAME)) + end + return Descriptor[direction].attributes[key] +end +Descriptor.attributes = {} +setmetatable(Descriptor.attributes, attribute_helper_mt) + +setmetatable(Descriptor, {__index = cluster_base}) + +return Descriptor + diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/PartsList.lua new file mode 100644 index 0000000000..be83b6f878 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/PartsList.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 PartsList = { + ID = 0x0003, + NAME = "PartsList", + base_type = require "st.matter.data_types.Array", + element_type = require "st.matter.data_types.Uint16", +} + +function PartsList: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, PartsList.element_type) + end +end + +function PartsList:new_value(...) + local o = self.base_type(table.unpack({...})) + + return o +end + +function PartsList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PartsList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function PartsList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function PartsList:build_test_report_data( + device, + endpoint_id, + value, + status +) + local data = data_types.validate_or_build_type(value, self.base_type) + + return cluster_base.build_test_report_data( + device, + endpoint_id, + self._cluster.ID, + self.ID, + data, + status + ) +end + +function PartsList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + + return data +end + +setmetatable(PartsList, {__call = PartsList.new_value, __index = PartsList.base_type}) +return PartsList + diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/TagList.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/TagList.lua new file mode 100644 index 0000000000..6bc889d407 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/TagList.lua @@ -0,0 +1,80 @@ +-- 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 TagList = { + ID = 0x0004, + NAME = "TagList", + _cluster = require "embedded_clusters.Descriptor", + base_type = require "st.matter.data_types.Array", + element_type = require "embedded_clusters.Global.types.SemanticTagStruct", +} + +function TagList: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, TagList.element_type) + end +end + +function TagList:new_value(...) + local o = self.base_type(table.unpack({...})) + self:augment_type(o) + return o +end + +function TagList:read(device, endpoint_id) + return cluster_base.read( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + + +function TagList:subscribe(device, endpoint_id) + return cluster_base.subscribe( + device, + endpoint_id, + self._cluster.ID, + self.ID, + nil + ) +end + +function TagList:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +function TagList: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 TagList:deserialize(tlv_buf) + local data = TLVParser.decode_tlv(tlv_buf) + self:augment_type(data) + return data +end + +setmetatable(TagList, {__call = TagList.new_value, __index = TagList.base_type}) +return TagList + diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/init.lua new file mode 100644 index 0000000000..ab933c601f --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/server/attributes/init.lua @@ -0,0 +1,27 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local attr_mt = {} +attr_mt.__attr_cache = {} +attr_mt.__index = function(self, key) + if attr_mt.__attr_cache[key] == nil then + local req_loc = string.format("embedded_clusters.Descriptor.server.attributes.%s", key) + local raw_def = require(req_loc) + local cluster = rawget(self, "_cluster") + raw_def:set_parent_cluster(cluster) + attr_mt.__attr_cache[key] = raw_def + end + return attr_mt.__attr_cache[key] +end + +local DescriptorServerAttributes = {} + +function DescriptorServerAttributes:set_parent_cluster(cluster) + self._cluster = cluster + return self +end + +setmetatable(DescriptorServerAttributes, attr_mt) + +return DescriptorServerAttributes + diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/types/Feature.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/types/Feature.lua new file mode 100644 index 0000000000..90c03f67b0 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/types/Feature.lua @@ -0,0 +1,56 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local Feature = {} +local new_mt = UintABC.new_mt({NAME = "Feature", ID = data_types.name_to_id_map["Uint32"]}, 4) + +Feature.BASE_MASK = 0xFFFF +Feature.TAG_LIST = 0x0001 + +Feature.mask_fields = { + BASE_MASK = 0xFFFF, + TAG_LIST = 0x0001, +} + +Feature.is_tag_list_set = function(self) + return (self.value & self.TAG_LIST) ~= 0 +end + +Feature.set_tag_list = function(self) + if self.value ~= nil then + self.value = self.value | self.TAG_LIST + else + self.value = self.TAG_LIST + end +end + +Feature.unset_tag_list = function(self) + self.value = self.value & (~self.TAG_LIST & self.BASE_MASK) +end + +function Feature.bits_are_valid(feature) + local max = + Feature.TAG_LIST + if (feature <= max) and (feature >= 1) then + return true + else + return false + end +end + +Feature.mask_methods = { + is_tag_list_set = Feature.is_tag_list_set, + set_tag_list = Feature.set_tag_list, + unset_tag_list = Feature.unset_tag_list, +} + +Feature.augment_type = function(_cls, val) + setmetatable(val, new_mt) +end + +setmetatable(Feature, new_mt) + +return Feature diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/types/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/types/init.lua new file mode 100644 index 0000000000..e76712b2af --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Descriptor/types/init.lua @@ -0,0 +1,9 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_loader = require "st.utils.lazy_loader" + +local DescriptorTypes = lazy_loader:new { prefix = "embedded_clusters.Descriptor.types." } + +return DescriptorTypes + diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/init.lua new file mode 100644 index 0000000000..5c0c673c40 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/init.lua @@ -0,0 +1,8 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local GlobalTypes = require "embedded_clusters.Global.types" + +local Global = {} +Global.types = GlobalTypes +return Global diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/SemanticTagStruct.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/SemanticTagStruct.lua new file mode 100644 index 0000000000..c0e6f8caa7 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/SemanticTagStruct.lua @@ -0,0 +1,94 @@ +-- Copyright © 2026 SmartThings, Inc. +-- 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 SemanticTagStruct = {} +local new_mt = StructureABC.new_mt({NAME = "SemanticTagStruct", ID = data_types.name_to_id_map["Structure"]}) + +SemanticTagStruct.field_defs = { + { + name = "mfg_code", + field_id = 0, + is_nullable = true, + is_optional = false, + data_type = require "st.matter.data_types.Uint16", + }, + { + name = "namespace_id", + field_id = 1, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "tag", + field_id = 2, + is_nullable = false, + is_optional = false, + data_type = require "st.matter.data_types.Uint8", + }, + { + name = "label", + field_id = 3, + is_nullable = true, + is_optional = true, + data_type = require "st.matter.data_types.UTF8String1", + }, +} + +SemanticTagStruct.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 + +SemanticTagStruct.serialize = function(self, buf, include_control, tag) + return data_types['Structure'].serialize(self.elements, buf, include_control, tag) +end + +new_mt.__call = SemanticTagStruct.init +new_mt.__index.serialize = SemanticTagStruct.serialize + +SemanticTagStruct.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(SemanticTagStruct, new_mt) + +return SemanticTagStruct + diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/ThreeLevelAutoEnum.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/ThreeLevelAutoEnum.lua new file mode 100644 index 0000000000..a203c9b505 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/ThreeLevelAutoEnum.lua @@ -0,0 +1,38 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local data_types = require "st.matter.data_types" +local UintABC = require "st.matter.data_types.base_defs.UintABC" + +local ThreeLevelAutoEnum = {} +local new_mt = UintABC.new_mt({NAME = "ThreeLevelAutoEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +new_mt.__index.pretty_print = function(self) + local name_lookup = { + [self.AUTO] = "AUTO", + [self.LOW] = "LOW", + [self.MEDIUM] = "MEDIUM", + [self.HIGH] = "HIGH", + } + return string.format("%s: %s", self.field_name or self.NAME, name_lookup[self.value] or string.format("%d", self.value)) +end +new_mt.__tostring = new_mt.__index.pretty_print + +new_mt.__index.AUTO = 0x00 +new_mt.__index.LOW = 0x01 +new_mt.__index.MEDIUM = 0x02 +new_mt.__index.HIGH = 0x03 + +ThreeLevelAutoEnum.AUTO = 0x00 +ThreeLevelAutoEnum.LOW = 0x01 +ThreeLevelAutoEnum.MEDIUM = 0x02 +ThreeLevelAutoEnum.HIGH = 0x03 + +ThreeLevelAutoEnum.augment_type = function(_cls, val) + setmetatable(val, new_mt) +end + +setmetatable(ThreeLevelAutoEnum, new_mt) + + +return ThreeLevelAutoEnum + diff --git a/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/init.lua b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/init.lua new file mode 100644 index 0000000000..f4b0f9b225 --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/embedded_clusters/Global/types/init.lua @@ -0,0 +1,18 @@ +-- Copyright © 2026 SmartThings, Inc. +-- 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.Global.types." .. key) + end + return types_mt.__types_cache[key] +end + +local GlobalTypes = {} + +setmetatable(GlobalTypes, types_mt) + + +return GlobalTypes diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua index a643fa909e..0ca3579b75 100644 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/can_handle.lua @@ -4,7 +4,8 @@ local CLOSURE_CONTROL_CLUSTER_ID = 0x0104 return function(opts, driver, device) - if #device:get_endpoints(CLOSURE_CONTROL_CLUSTER_ID) > 0 then + local embedded_cluster_utils = require "sub_drivers.closure.closure_utils.embedded_cluster_utils" + if #embedded_cluster_utils.get_endpoints(device, CLOSURE_CONTROL_CLUSTER_ID) > 0 then return true, require("sub_drivers.closure") end return false diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua index c574991c82..24faac339b 100644 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_handlers/attribute_handlers.lua @@ -8,6 +8,7 @@ local version = require "version" if version.api < 20 then clusters.ClosureControl = require "embedded_clusters.ClosureControl" clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" + clusters.Global = require "embedded_clusters.Global" end local fields = require "sub_drivers.closure.closure_utils.fields" @@ -63,6 +64,9 @@ end function ClosureAttrHandlers.tag_list_handler(driver, device, ib, response) if not ib.data.elements then return end local tag_value + if version.api < 16 then + clusters.Global.types.SemanticTagStruct:augment_type(ib.data) + end for _, v in ipairs(ib.data.elements) do local tag = v.elements if tag and tag.namespace_id and tag.namespace_id.value == 0x44 then diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/embedded_cluster_utils.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/embedded_cluster_utils.lua new file mode 100644 index 0000000000..022f86987a --- /dev/null +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/embedded_cluster_utils.lua @@ -0,0 +1,62 @@ +-- Copyright © 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local utils = require "st.utils" +local version = require "version" + +if version.api < 20 then + clusters.ClosureControl = require "embedded_clusters.ClosureControl" + clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + +local embedded_cluster_utils = {} + +local embedded_clusters_16 = { + [clusters.Descriptor.ID] = clusters.Descriptor +} + +local embedded_clusters_20 = { + [clusters.ClosureControl.ID] = clusters.ClosureControl, + [clusters.ClosureDimension.ID] = clusters.ClosureDimension +} + +function embedded_cluster_utils.get_endpoints(device, cluster_id, opts) + -- If using older lua libs and need to check for an embedded cluster feature, + -- we must use the embedded cluster definitions here + if (version.api < 16 and embedded_clusters_16[cluster_id] ~= nil) + or (version.api < 20 and embedded_clusters_20[cluster_id] ~= nil) then + local embedded_cluster = embedded_clusters_16[cluster_id] or embedded_clusters_20[cluster_id] + local opts = opts or {} + if utils.table_size(opts) > 1 then + device.log.warn_with({hub_logs = true}, "Invalid options for get_endpoints") + return + end + local clus_has_features = function(clus, feature_bitmap) + if not feature_bitmap or not clus then return false end + return embedded_cluster.are_features_supported(feature_bitmap, clus.feature_map) + end + local eps = {} + for _, ep in ipairs(device.endpoints) do + for _, clus in ipairs(ep.clusters) do + if ((clus.cluster_id == cluster_id) + and (opts.feature_bitmap == nil or clus_has_features(clus, opts.feature_bitmap)) + and ((opts.cluster_type == nil and clus.cluster_type == "SERVER" or clus.cluster_type == "BOTH") + or (opts.cluster_type == clus.cluster_type)) + or (cluster_id == nil)) then + table.insert(eps, ep.endpoint_id) + if cluster_id == nil then break end + end + end + end + return eps + else + return device:get_endpoints(cluster_id, opts) + end + end + + return embedded_cluster_utils diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua index df6dc9015c..8597d029f5 100644 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/closure_utils/utils.lua @@ -13,12 +13,13 @@ if version.api < 20 then end local fields = require "sub_drivers.closure.closure_utils.fields" +local embedded_cluster_utils = require "sub_drivers.closure.closure_utils.embedded_cluster_utils" local utils = {} function utils.find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT - local eps = device:get_endpoints(cluster) + local eps = embedded_cluster_utils.get_endpoints(device, cluster) table.sort(eps) for _, v in ipairs(eps) do if v ~= 0 then -- 0 is the Matter RootNode endpoint @@ -33,7 +34,7 @@ function utils.find_default_endpoint(device, cluster) end function utils.get_closure_dimension_eps(device) - local eps = device:get_endpoints(clusters.ClosureDimension.ID) or {} + local eps = embedded_cluster_utils.get_endpoints(device, clusters.ClosureDimension.ID) or {} table.sort(eps) local result = {} for _, ep in ipairs(eps) do diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua index 638f561415..397167d3df 100644 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/closure/init.lua @@ -5,12 +5,19 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local log = require "log" local version = require "version" +local embedded_cluster_utils = require "sub_drivers.closure.closure_utils.embedded_cluster_utils" if version.api < 20 then clusters.ClosureControl = require "embedded_clusters.ClosureControl" clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" + clusters.Global = require "embedded_clusters.Global" end +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + + local fields = require "sub_drivers.closure.closure_utils.fields" local closure_utils = require "sub_drivers.closure.closure_utils.utils" local attribute_handlers = require "sub_drivers.closure.closure_handlers.attribute_handlers" @@ -55,7 +62,7 @@ function ClosureLifecycleHandlers.device_added(driver, device) end function ClosureLifecycleHandlers.do_configure(driver, device) - if #device:get_endpoints(clusters.Descriptor.ID) == 0 then + if #embedded_cluster_utils.get_endpoints(device, clusters.Descriptor.ID) == 0 then log.warn( "Descriptor cluster not implemented on ClosureControl endpoint, " .. "cannot read TagList to determine closure type" diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua index 56407c6667..5db95ba28c 100644 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua @@ -119,7 +119,7 @@ local matter_window_covering_position_updates_while_moving_handler = { }, capability_handlers = { }, - can_handle = require("matter-window-covering-position-updates-while-moving.can_handle"), + can_handle = require("sub_drivers.matter-window-covering-position-updates-while-moving.can_handle"), shared_device_thread_enabled = true, } diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua index 95a4c6f112..e39cb428c8 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua @@ -11,6 +11,11 @@ local version = require "version" if version.api < 20 then clusters.ClosureControl = require "embedded_clusters.ClosureControl" clusters.ClosureDimension = require "embedded_clusters.ClosureDimension" + clusters.Global = require "embedded_clusters.Global" +end + +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" end -- --------------------------------------------------------------------------- diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index 9f273037f3..cf2b9fd681 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -8,6 +8,11 @@ local t_utils = require "integration_test.utils" local uint32 = require "st.matter.data_types.Uint32" local clusters = require "st.matter.clusters" +local version = require "version" +if version.api < 16 then + clusters.Descriptor = require "embedded_clusters.Descriptor" +end + local WindowCovering = clusters.WindowCovering test.disable_startup_messages() From f8c68740466e0b9fd7de4c9c6f75b8cc601e2d09 Mon Sep 17 00:00:00 2001 From: Kwang-Hui Cho Date: Tue, 7 Jul 2026 08:15:43 +0900 Subject: [PATCH 018/102] Add eufy product --- .../SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua index 4b15b23b2b..fef7746990 100644 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua +++ b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua @@ -28,6 +28,7 @@ local NEW_MATTER_LOCK_PRODUCTS = { {0x1533, 0x0012}, -- eufy, FamiLock E35 {0x1533, 0x0016}, -- eufy, FamiLock E32 {0x1533, 0x0014}, -- eufy, FamiLock E40 + {0x1533, 0x001F}, -- eufy, EAP {0x135D, 0x00B1}, -- Nuki, Smart Lock Pro {0x135D, 0x00B2}, -- Nuki, Smart Lock {0x135D, 0x00C1}, -- Nuki, Smart Lock From 7b18436b90cee44a916c873fcfd598d8a00cac81 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Tue, 7 Jul 2026 11:58:24 -0500 Subject: [PATCH 019/102] Add device profile for U600 (#3097) --- drivers/SmartThings/matter-lock/fingerprints.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 575da17382..6d4d068b71 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -39,6 +39,7 @@ matterManufacturer: deviceLabel: Aqara Smart Lock U600 vendorId: 0x115F productId: 0x284A + deviceProfileName: lock-modular - id: "4447/10253" deviceLabel: Aqara Smart Lock J200 vendorId: 0x115F From ed47433bd2f33bdf7252192baf855a979dccf5bd Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Fri, 15 May 2026 13:47:27 -0500 Subject: [PATCH 020/102] Starting of passing PR number --- .github/workflows/jenkins-driver-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/jenkins-driver-tests.yml b/.github/workflows/jenkins-driver-tests.yml index 6494eadd15..aff7f6f150 100644 --- a/.github/workflows/jenkins-driver-tests.yml +++ b/.github/workflows/jenkins-driver-tests.yml @@ -40,6 +40,7 @@ jobs: -H "Authorization: Bearer ${JENKINS_WEBHOOK_TOKEN}" \ -d "{\"status_url\": \"$STATUS_URL\", \"version\": ${{ matrix.version }}, - \"commit\": ${{ github.event.pull_request.head.sha }} }" \ + \"commit\": ${{ github.event.pull_request.head.sha }}, + \"pr_id\": ${{ github.event.number }} }" \ "${JENKINS_WEBHOOK_URL}" set -x From d09e25692d00e51a83112cafbaf5e55dc5519420 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:46:57 -0500 Subject: [PATCH 021/102] Z-Wave Lock: Support SLGA Migration (#3087) --- .../zwave-lock/profiles/base-lock-tamper.yml | 4 + .../zwave-lock/profiles/base-lock.yml | 4 + drivers/SmartThings/zwave-lock/src/init.lua | 225 ++---- .../zwave-lock/src/keywe-lock/can_handle.lua | 10 +- .../zwave-lock/src/keywe-lock/init.lua | 8 +- .../src/legacy-handlers/can_handle.lua | 12 + .../zwave-lock/src/legacy-handlers/init.lua | 133 +++ .../legacy-handlers/keywe-lock/can_handle.lua | 12 + .../src/legacy-handlers/keywe-lock/init.lua | 71 ++ .../samsung-lock/can_handle.lua | 11 + .../src/legacy-handlers/samsung-lock/init.lua | 96 +++ .../schlage-lock/can_handle.lua | 11 + .../src/legacy-handlers/schlage-lock/init.lua | 178 ++++ .../src/legacy-handlers/sub_drivers.lua | 11 + .../zwave-alarm-v1-lock/can_handle.lua | 10 + .../zwave-alarm-v1-lock/init.lua | 146 ++++ .../src/lock_handlers/capabilities.lua | 252 ++++++ .../src/lock_handlers/zwave_responses.lua | 243 ++++++ .../zwave-lock/src/lock_utils/constants.lua | 48 ++ .../zwave-lock/src/lock_utils/tables.lua | 268 ++++++ .../zwave-lock/src/lock_utils/utils.lua | 160 ++++ .../src/samsung-lock/can_handle.lua | 17 +- .../zwave-lock/src/samsung-lock/init.lua | 60 +- .../src/schlage-lock/can_handle.lua | 17 +- .../zwave-lock/src/schlage-lock/init.lua | 133 +-- .../zwave-lock/src/sub_drivers.lua | 1 + .../src/test/test_init_lifecycle_handlers.lua | 198 +++++ .../zwave-lock/src/test/test_keywe_lock.lua | 84 +- .../src/test/test_keywe_lock_legacy.lua | 101 +++ .../test/test_lock_code_slga_migration.lua | 127 +++ .../test/test_lock_credentials_commands.lua | 480 +++++++++++ .../src/test/test_lock_pre_configured.lua | 514 ++++++++++++ .../zwave-lock/src/test/test_lock_tables.lua | 761 ++++++++++++++++++ .../src/test/test_lock_users_commands.lua | 705 ++++++++++++++++ .../zwave-lock/src/test/test_samsung_lock.lua | 190 +++-- .../src/test/test_samsung_lock_legacy.lua | 183 +++++ .../zwave-lock/src/test/test_schlage_lock.lua | 189 +++-- .../src/test/test_schlage_lock_legacy.lua | 266 ++++++ .../test/test_zwave_lock_code_migration.lua | 251 ------ ...ve_lock.lua => test_zwave_lock_legacy.lua} | 41 +- .../src/test/test_zwave_responses.lua | 393 +++++++++ .../src/zwave-alarm-v1-lock/can_handle.lua | 15 +- .../src/zwave-alarm-v1-lock/init.lua | 100 +-- 43 files changed, 5866 insertions(+), 873 deletions(-) create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/keywe-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/keywe-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/sub_drivers.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/can_handle.lua create mode 100644 drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/init.lua create mode 100644 drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua create mode 100644 drivers/SmartThings/zwave-lock/src/lock_handlers/zwave_responses.lua create mode 100644 drivers/SmartThings/zwave-lock/src/lock_utils/constants.lua create mode 100644 drivers/SmartThings/zwave-lock/src/lock_utils/tables.lua create mode 100644 drivers/SmartThings/zwave-lock/src/lock_utils/utils.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_lock_code_slga_migration.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_lock_pre_configured.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_lock_tables.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua rename drivers/SmartThings/zwave-lock/src/test/{test_zwave_lock.lua => test_zwave_lock_legacy.lua} (97%) create mode 100644 drivers/SmartThings/zwave-lock/src/test/test_zwave_responses.lua diff --git a/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml b/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml index 5fbfb13f3d..e3a25ab57a 100644 --- a/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml +++ b/drivers/SmartThings/zwave-lock/profiles/base-lock-tamper.yml @@ -6,6 +6,10 @@ components: version: 1 - id: lockCodes version: 1 + - id: lockCredentials + version: 1 + - id: lockUsers + version: 1 - id: battery version: 1 - id: tamperAlert diff --git a/drivers/SmartThings/zwave-lock/profiles/base-lock.yml b/drivers/SmartThings/zwave-lock/profiles/base-lock.yml index f4957f9ad0..efb97c8b27 100644 --- a/drivers/SmartThings/zwave-lock/profiles/base-lock.yml +++ b/drivers/SmartThings/zwave-lock/profiles/base-lock.yml @@ -6,6 +6,10 @@ components: version: 1 - id: lockCodes version: 1 + - id: lockCredentials + version: 1 + - id: lockUsers + version: 1 - id: battery version: 1 - id: refresh diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index c3506c5005..8b2cf76eb8 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -1,175 +1,120 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local capabilities = require "st.capabilities" ---- @type st.zwave.CommandClass -local cc = require "st.zwave.CommandClass" --- @type st.zwave.Driver local ZwaveDriver = require "st.zwave.driver" --- @type st.zwave.defaults local defaults = require "st.zwave.defaults" ---- @type st.zwave.CommandClass.DoorLock -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" --- @type st.zwave.CommandClass.UserCode local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) ---- @type st.zwave.CommandClass.Battery -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) ---- @type st.zwave.CommandClass.Time -local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) -local constants = require "st.zwave.constants" -local utils = require "st.utils" -local json = require "st.json" +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local SCAN_CODES_CHECK_INTERVAL = 30 -local MIGRATION_COMPLETE = "migrationComplete" -local MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped" +local capabilities = require "st.capabilities" -local function periodic_codes_state_verification(driver, device) - local scan_codes_state = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.scanCodes.NAME) - if scan_codes_state == "Scanning" then - driver:inject_capability_command(device, - { capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - } - ) - device.thread:call_with_delay( - SCAN_CODES_CHECK_INTERVAL, - function(d) - periodic_codes_state_verification(driver, device) - end - ) - end -end +local consts = require "lock_utils.constants" +local lock_utils = require "lock_utils.utils" +local table_utils = require "lock_utils.tables" +local zwave_handlers = require "lock_handlers.zwave_responses" +local capability_handlers = require "lock_handlers.capabilities" -local function populate_state_from_data(device) - if device.data.lockCodes ~= nil and device:get_field(MIGRATION_COMPLETE) ~= true then - -- build the lockCodes table - local lockCodes = {} - local lc_data = json.decode(device.data.lockCodes) - for k, v in pairs(lc_data) do - lockCodes[k] = v - end - -- Populate the devices `lockCodes` field - device:set_field(constants.LOCK_CODES, utils.deep_copy(lockCodes), { persist = true }) - -- Populate the devices state history cache - device.state_cache["main"] = device.state_cache["main"] or {} - device.state_cache["main"][capabilities.lockCodes.ID] = device.state_cache["main"][capabilities.lockCodes.ID] or {} - device.state_cache["main"][capabilities.lockCodes.ID][capabilities.lockCodes.lockCodes.NAME] = {value = json.encode(utils.deep_copy(lockCodes))} - device:set_field(MIGRATION_COMPLETE, true, { persist = true }) - end -end +local LockLifecycle = {} ---- Builds up initial state for the device ---- ---- @param self st.zwave.Driver ---- @param device st.zwave.Device -local function added_handler(self, device) - populate_state_from_data(device) - if device.data.lockCodes == nil or device:get_field(MIGRATION_RELOAD_SKIPPED) == true then - if (device:supports_capability(capabilities.lockCodes)) then - self:inject_capability_command(device, - { capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} }) - device.thread:call_with_delay( - SCAN_CODES_CHECK_INTERVAL, - function(d) - periodic_codes_state_verification(self, device) - end - ) - end - else - device:set_field(MIGRATION_RELOAD_SKIPPED, true, { persist = true }) +function LockLifecycle.device_added(driver, device) + if device:supports_capability(capabilities.lockCodes) and device._provisioning_state == "TYPED" then + -- set the migrated field to true so new devices use lockCredentials/lockUsers from the start. + -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, + -- and should be migrated manually by the user. + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) end - device:send(DoorLock:OperationGet({})) - device:send(Battery:Get({})) - if (device:supports_capability(capabilities.tamperAlert)) then + if device:supports_capability(capabilities.tamperAlert) then device:emit_event(capabilities.tamperAlert.tamper.clear()) end + -- set initial state + driver:inject_capability_command(device, { + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} + }) + -- ensure our user/credential state is accurate to the current device state + device.thread:call_with_delay(5, function() lock_utils.sync_device_state(device) end) end -local init_handler = function(driver, device, event) - populate_state_from_data(device) - -- temp fix before this can be changed from being persisted in memory - device:set_field(constants.CODE_STATE, nil, { persist = true }) -end +function LockLifecycle.init(driver, device) + -- Restore users/credentials capability state from the persistent store in case + -- the capability state cache was wiped since the last driver run. + table_utils.restore_from_persistent_store(device) -local do_refresh = function(self, device) - device:send(DoorLock:OperationGet({})) - device:send(Battery:Get({})) -end + local lock_pins_supported_by_profile = device:supports_capability(capabilities.lockCodes) + if lock_pins_supported_by_profile and device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) == true then + -- ensure lockCodes capability state is reflected correctly for already migrated devices + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({ consts.CRED_TYPE_PIN }, { visibility = { displayed = false } })) + end ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @param cmd table -local function update_codes(driver, device, cmd) - local delay = 0 - -- args.codes is json - for name, code in pairs(cmd.args.codes) do - -- these seem to come in the format "code[slot#]: code" - local code_slot = tonumber(string.gsub(name, "code", ""), 10) - if (code_slot ~= nil) then - if (code ~= nil and (code ~= "0" and code ~= "")) then - -- code changed - device.thread:call_with_delay(delay, function () - device:send(UserCode:Set({ - user_identifier = code_slot, - user_code = code, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) - end) - delay = delay + 2.2 - else - -- code deleted - device.thread:call_with_delay(delay, function () - device:send(UserCode:Set({user_identifier = code_slot, user_id_status = UserCode.user_id_status.AVAILABLE})) - end) - delay = delay + 2.2 - device.thread:call_with_delay(delay, function () - device:send(UserCode:Get({user_identifier = code_slot})) - end) - delay = delay + 2.2 - end - end + if device:supports_capability(capabilities.tamperAlert) then + -- ensure our user/credential state is accurate to the current device state + device:emit_event(capabilities.tamperAlert.tamper.clear()) end end -local function time_get_handler(driver, device, cmd) - local time = os.date("*t") - device:send_to_component( - Time:Report({ - hour_local_time = time.hour, - minute_local_time = time.min, - second_local_time = time.sec - }), - device:endpoint_to_component(cmd.src_channel) - ) +function LockLifecycle.driver_switched(driver, device) + if device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME) == true then + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + end + device:try_update_metadata({ provisioning_state = "PROVISIONED" }) end local driver_template = { - supported_capabilities = { - capabilities.lock, - capabilities.lockCodes, - capabilities.battery, - capabilities.tamperAlert - }, lifecycle_handlers = { - added = added_handler, - init = init_handler, + added = LockLifecycle.device_added, + driver_switched = LockLifecycle.driver_switched, + init = LockLifecycle.init, + }, + zwave_handlers = { + [cc.TIME] = { + [0x01] = zwave_handlers.time_get_handler -- used by DanaLock + }, + [cc.NOTIFICATION] = { + [Notification.REPORT] = zwave_handlers.notification_report + }, + [cc.USER_CODE] = { + [UserCode.REPORT] = zwave_handlers.user_code_report, + [UserCode.USERS_NUMBER_REPORT] = zwave_handlers.users_number_report, + } }, capability_handlers = { - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes + [capabilities.lock.ID] = { + [capabilities.lock.commands.lock.NAME] = capability_handlers.lock, + [capabilities.lock.commands.unlock.NAME] = capability_handlers.unlock, + }, + [capabilities.lockUsers.ID] = { + [capabilities.lockUsers.commands.addUser.NAME] = capability_handlers.add_user, + [capabilities.lockUsers.commands.updateUser.NAME] = capability_handlers.update_user, + [capabilities.lockUsers.commands.deleteUser.NAME] = capability_handlers.delete_user, + [capabilities.lockUsers.commands.deleteAllUsers.NAME] = capability_handlers.delete_all_users, + }, + [capabilities.lockCredentials.ID] = { + [capabilities.lockCredentials.commands.addCredential.NAME] = capability_handlers.add_credential, + [capabilities.lockCredentials.commands.updateCredential.NAME] = capability_handlers.update_credential, + [capabilities.lockCredentials.commands.deleteCredential.NAME] = capability_handlers.delete_credential, + [capabilities.lockCredentials.commands.deleteAllCredentials.NAME] = capability_handlers.delete_all_credentials, }, [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = do_refresh - } + [capabilities.refresh.commands.refresh.NAME] = capability_handlers.refresh, + }, }, - zwave_handlers = { - [cc.TIME] = { - [Time.GET] = time_get_handler -- used by DanaLock - } + supported_capabilities = { + capabilities.lock, + capabilities.lockCodes, + capabilities.lockUsers, + capabilities.lockCredentials, + capabilities.battery, + capabilities.tamperAlert, }, sub_drivers = require("sub_drivers"), shared_device_thread_enabled = true, diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua index d8bcd5756e..fda854593d 100644 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/can_handle.lua @@ -2,9 +2,13 @@ -- Licensed under the Apache License, Version 2.0 local function can_handle_keywe_lock(opts, self, device, cmd, ...) - local KEYWE_MFR = 0x037B - if device.zwave_manufacturer_id == KEYWE_MFR then - return true, require("keywe-lock") + local consts = require("lock_utils.constants") + local slga_migrated = device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) + if slga_migrated then + local KEYWE_MFR = 0x037B + if device.zwave_manufacturer_id == KEYWE_MFR then + return true, require("legacy-handlers.keywe-lock") + end end return false end diff --git a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua index a51af26e00..455d301473 100644 --- a/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/keywe-lock/init.lua @@ -9,9 +9,8 @@ local Association = (require "st.zwave.CommandClass.Association")({version=2}) local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local access_control_event = Notification.event.access_control -local LockDefaults = require "st.zwave.defaults.lock" -local LockCodesDefaults = require "st.zwave.defaults.lockCodes" local TamperDefaults = require "st.zwave.defaults.tamperAlert" +local zwave_handlers = require "lock_handlers.zwave_responses" local TAMPER_CLEAR_DELAY = 10 @@ -39,8 +38,9 @@ local function notification_report_handler(self, device, cmd) if event ~= nil then device:emit_event(event) else - LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + zwave_handlers.door_operation_event_handler(self, device, cmd) + zwave_handlers.code_event_handler(self, device, cmd) + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) device.thread:call_with_delay( TAMPER_CLEAR_DELAY, diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua new file mode 100644 index 0000000000..78757208a4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local consts = require("lock_utils.constants") + local slga_migrated = device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) + if not slga_migrated then + local subdriver = require("legacy-handlers") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua new file mode 100644 index 0000000000..3bcb8adfe4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/init.lua @@ -0,0 +1,133 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local LockDefaults = require "st.zwave.defaults.lock" +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" + +local init_handler = function(driver, device, event) + local constants = require "st.zwave.constants" + -- temp fix before this can be changed from being persisted in memory + device:set_field(constants.CODE_STATE, nil, { persist = true }) +end + +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd table +local function update_codes(driver, device, cmd) + local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + local delay = 0 + -- args.codes is json + for name, code in pairs(cmd.args.codes) do + -- these seem to come in the format "code[slot#]: code" + local code_slot = tonumber(string.gsub(name, "code", ""), 10) + if (code_slot ~= nil) then + if (code ~= nil and (code ~= "0" and code ~= "")) then + -- code changed + device.thread:call_with_delay(delay, function () + device:send(UserCode:Set({ + user_identifier = code_slot, + user_code = code, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS})) + end) + delay = delay + 2.2 + else + -- code deleted + device.thread:call_with_delay(delay, function () + device:send(UserCode:Set({user_identifier = code_slot, user_id_status = UserCode.user_id_status.AVAILABLE})) + end) + delay = delay + 2.2 + device.thread:call_with_delay(delay, function () + device:send(UserCode:Get({user_identifier = code_slot})) + end) + delay = delay + 2.2 + end + end + end +end + +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd table +local function migrate(driver, device, cmd) + local LockCodesDefaults = require "st.zwave.defaults.lockCodes" + local get_lock_codes = LockCodesDefaults.get_lock_codes + local lock_users = {} + local lock_credentials = {} + local lock_codes = get_lock_codes(device) + local ordered_codes = {} + + for code in pairs(lock_codes) do + table.insert(ordered_codes, code) + end + + table.sort(ordered_codes) + for index = 1, #ordered_codes do + local code_slot, code_name = ordered_codes[index], lock_codes[ ordered_codes[index] ] + table.insert(lock_users, {userIndex = index, userType = "guest", userName = code_name}) + table.insert(lock_credentials, {userIndex = index, credentialIndex = tonumber(code_slot), credentialType = "pin"}) + end + device:emit_event(capabilities.lockCredentials.credentials(lock_credentials, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockUsers.users(lock_users, { visibility = { displayed = false } })) + + local code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if code_length then + device:emit_event(capabilities.lockCredentials.minPinCodeLen(code_length, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(code_length, { visibility = { displayed = false } })) + end + + local max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) + if max_codes then + device:emit_event(capabilities.lockCredentials.pinUsersSupported(max_codes, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockUsers.totalUsersSupported(max_codes, { visibility = { displayed = false } })) + else + -- if we don't have a code length, request it from the device + device:send(UserCode:UsersNumberGet({})) + end + + device:emit_event(capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + local consts = require("lock_utils.constants") + + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) -- persist the migrated state to the datastore +end + +local legacy_capabilities = { + supported_capabilities = { + capabilities.lock, + capabilities.lockCodes, + capabilities.battery, + capabilities.tamperAlert + }, + lifecycle_handlers = { + init = init_handler, + }, + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.updateCodes.NAME] = update_codes, + [capabilities.lockCodes.commands.migrate.NAME] = migrate + }, + }, + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = function(driver, device, cmd) + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) + local TamperDefaults = require "st.zwave.defaults.tamperAlert" + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) + end + }, + [cc.USER_CODE] = { + [UserCode.REPORT] = LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT], + [UserCode.USERS_NUMBER_REPORT] = LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.USERS_NUMBER_REPORT], + } + }, + sub_drivers = require("legacy-handlers.sub_drivers"), + can_handle = require("legacy-handlers.can_handle"), + NAME = "legacy-handlers" +} + +return legacy_capabilities diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/keywe-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/keywe-lock/can_handle.lua new file mode 100644 index 0000000000..0970a9c9ed --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/keywe-lock/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function can_handle_keywe_lock(opts, self, device, cmd, ...) + local KEYWE_MFR = 0x037B + if device.zwave_manufacturer_id == KEYWE_MFR then + return true, require("legacy-handlers.keywe-lock") + end + return false +end + +return can_handle_keywe_lock diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/keywe-lock/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/keywe-lock/init.lua new file mode 100644 index 0000000000..b40e6658b1 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/keywe-lock/init.lua @@ -0,0 +1,71 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" + +local Association = (require "st.zwave.CommandClass.Association")({version=2}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control + +local LockDefaults = require "st.zwave.defaults.lock" +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" +local TamperDefaults = require "st.zwave.defaults.tamperAlert" + +local TAMPER_CLEAR_DELAY = 10 + +local function clear_tamper_if_needed(device) + local current_tamper_state = device:get_latest_state("main", capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME) + if current_tamper_state == "detected" then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +local function notification_report_handler(self, device, cmd) + local event + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event_code = cmd.args.event + if event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_OPEN then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.WINDOW_DOOR_HANDLE_IS_CLOSED then + event = capabilities.lock.lock.locked() + end + if event ~= nil then + event["data"] = {method = "manual"} + end + end + + if event ~= nil then + device:emit_event(event) + else + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + device.thread:call_with_delay( + TAMPER_CLEAR_DELAY, + function(d) + clear_tamper_if_needed(device) + end + ) + end +end + +local function do_configure(self, device) + device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local keywe_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Keywe Lock", + can_handle = require("keywe-lock.can_handle"), +} + +return keywe_lock \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/can_handle.lua new file mode 100644 index 0000000000..dc97d03ece --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local SAMSUNG_MFR = 0x022E + if device.zwave_manufacturer_id == SAMSUNG_MFR then + local subdriver = require("legacy-handlers.samsung-lock") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/init.lua new file mode 100644 index 0000000000..6e28668b0b --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/samsung-lock/init.lua @@ -0,0 +1,96 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" + +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local access_control_event = Notification.event.access_control + +local json = require "dkjson" +local constants = require "st.zwave.constants" + +local LockDefaults = require "st.zwave.defaults.lock" +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" +local get_lock_codes = LockCodesDefaults.get_lock_codes +local clear_code_state = LockCodesDefaults.clear_code_state +local code_deleted = LockCodesDefaults.code_deleted + + +local function get_ongoing_code_set(device) + local code_id + local code_state = device:get_field(constants.CODE_STATE) + if code_state ~= nil then + for key, state in pairs(code_state) do + if state ~= nil then + code_id = key:match("setName(%d)") + end + end + end + return code_id +end + +local function notification_report_handler(self, device, cmd) + local event + if (cmd.args.notification_type == Notification.notification_type.ACCESS_CONTROL) then + local event_code = cmd.args.event + if event_code == access_control_event.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then + event = capabilities.lock.lock.unlocked() + elseif event_code == access_control_event.NEW_USER_CODE_ADDED then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + device:send(UserCode:Get({user_identifier = code_id})) + return + end + elseif event_code == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then + local code_id = get_ongoing_code_set(device) + if code_id ~= nil then + event = capabilities.lockCodes.codeChanged(code_id .. " failed", { state_change = true }) + clear_code_state(device, code_id) + end + elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then + -- Update Master Code in the same way as in defaults... + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + -- ...and delete rest of them, as lock does + local lock_codes = get_lock_codes(device) + for code_id, _ in pairs(lock_codes) do + if code_id ~= "0" then + code_deleted(device, code_id) + end + end + event = capabilities.lockCodes.lockCodes(json.encode(get_lock_codes(device)), { visibility = { displayed = false } }) + end + end + + if event ~= nil then + device:emit_event(event) + else + LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + end +end + +-- Used doConfigure instead of added to not overwrite parent driver's added_handler +local function do_configure(self, device) + -- taken directly from DTH + -- Samsung locks won't allow you to enter the pairing menu when locked, so it must be unlocked + device:emit_event(capabilities.lock.lock.unlocked()) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } })) +end + +local samsung_lock = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure + }, + NAME = "Samsung Lock", + can_handle = require("legacy-handlers.samsung-lock.can_handle"), +} + +return samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/can_handle.lua new file mode 100644 index 0000000000..e45491efe2 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local SCHLAGE_MFR = 0x003B + if device.zwave_manufacturer_id == SCHLAGE_MFR then + local subdriver = require("legacy-handlers.schlage-lock") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/init.lua new file mode 100644 index 0000000000..a6604b01f5 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/schlage-lock/init.lua @@ -0,0 +1,178 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +local cc = require "st.zwave.CommandClass" +local constants = require "st.zwave.constants" +local json = require "dkjson" + +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local user_id_status = UserCode.user_id_status +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local access_control_event = Notification.event.access_control +local Configuration = (require "st.zwave.CommandClass.Configuration")({version=2}) +local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) +local Association = (require "st.zwave.CommandClass.Association")({version=1}) + +local LockCodesDefaults = require "st.zwave.defaults.lockCodes" + +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds + +local function set_code_length(self, device, cmd) + local length = cmd.args.length + if length >= 4 and length <= 8 then + device:send(Configuration:Set({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = length, + size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size + })) + end +end + +local function reload_all_codes(self, device, cmd) + LockCodesDefaults.capability_handlers[capabilities.lockCodes.commands.reloadAllCodes](self, device, cmd) + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + end +end + +local function set_code(self, device, cmd) + if (cmd.args.codePIN == "") then + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = {cmd.args.codeSlot, cmd.args.codeName}, + }) + else + -- copied from defaults with additional check for Schlage's configuration + if (cmd.args.codeName ~= nil and cmd.args.codeName ~= "") then + if (device:get_field(constants.CODE_STATE) == nil) then device:set_field(constants.CODE_STATE, { persist = true }) end + local code_state = device:get_field(constants.CODE_STATE) + code_state["setName"..cmd.args.codeSlot] = cmd.args.codeName + device:set_field(constants.CODE_STATE, code_state, { persist = true }) + end + local send_set_user_code = function () + device:send(UserCode:Set({ + user_identifier = cmd.args.codeSlot, + user_code = cmd.args.codePIN, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + end + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device.thread:call_with_delay(DEFAULT_COMMANDS_DELAY, send_set_user_code) + else + send_set_user_code() + end + end +end + +local function do_configure(self, device) + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local function basic_set_handler(self, device, cmd) + device:emit_event(cmd.args.value == 0 and capabilities.lock.lock.unlocked() or capabilities.lock.lock.locked()) + device:send(Association:Remove({grouping_identifier = 1, node_ids = {self.environment_info.hub_zwave_id}})) +end + +local function configuration_report(self, device, cmd) + local parameter_number = cmd.args.parameter_number + if parameter_number == SCHLAGE_LOCK_CODE_LENGTH_PARAM.number then + local reported_code_length = cmd.args.configuration_value + local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + if current_code_length ~= nil and current_code_length ~= reported_code_length then + local all_codes_deleted_mocked_command = Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.ALL_USER_CODES_DELETED + }) + LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, all_codes_deleted_mocked_command) + end + device:emit_event(capabilities.lockCodes.codeLength(reported_code_length)) + end +end + +local function is_user_code_report_mfr_specific(device, cmd) + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local code_id = cmd.args.user_identifier + + if reported_user_id_status == user_id_status.ENABLED_GRANT_ACCESS or -- OCCUPIED in UserCodeV1 + (reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE and user_code ~= nil) then + local code_state = device:get_field(constants.CODE_STATE) + return user_code == "**********" or user_code == nil or (code_state ~= nil and code_state["setName"..cmd.args.user_identifier] ~= nil) + else + return (code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE) or + reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE + end +end + +local function user_code_report_handler(self, device, cmd) + local code_id = cmd.args.user_identifier + if is_user_code_report_mfr_specific(device, cmd) then + local reported_user_id_status = cmd.args.user_id_status + local user_code = cmd.args.user_code + local event + + if reported_user_id_status == user_id_status.ENABLED_GRANT_ACCESS or -- OCCUPIED in UserCodeV1 + (reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE and user_code ~= nil) then + local code_name = LockCodesDefaults.get_code_name(device, code_id) + local change_type = LockCodesDefaults.get_change_type(device, code_id) + event = capabilities.lockCodes.codeChanged(code_id..""..change_type, { state_change = true }) + event.data = {codeName = code_name} + if code_id ~= 0 then -- ~= MASTER_CODE + LockCodesDefaults.code_set_event(device, code_id, code_name) + end + elseif code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE then + local lock_codes = LockCodesDefaults.get_lock_codes(device) + for _code_id, _ in pairs(lock_codes) do + LockCodesDefaults.code_deleted(device, _code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(LockCodesDefaults.get_lock_codes(device)), { visibility = { displayed = false } })) + else -- user_id_status.STATUS_NOT_AVAILABLE + event = capabilities.lockCodes.codeChanged(code_id.." failed", { state_change = true }) + end + + if event ~= nil then + device:emit_event(event) + end + LockCodesDefaults.clear_code_state(device, code_id) + LockCodesDefaults.verify_set_code_completion(device, cmd, code_id) + else + LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + end +end + +local schlage_lock = { + capability_handlers = { + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.setCodeLength.NAME] = set_code_length, + [capabilities.lockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [capabilities.lockCodes.commands.setCode.NAME] = set_code + } + }, + zwave_handlers = { + [cc.USER_CODE] = { + [UserCode.REPORT] = user_code_report_handler + }, + [cc.CONFIGURATION] = { + [Configuration.REPORT] = configuration_report + }, + [cc.BASIC] = { + [Basic.SET] = basic_set_handler + } + }, + lifecycle_handlers = { + doConfigure = do_configure, + }, + NAME = "Schlage Lock", + can_handle = require("legacy-handlers.schlage-lock.can_handle"), +} + +return schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/sub_drivers.lua new file mode 100644 index 0000000000..4c94ca5559 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/sub_drivers.lua @@ -0,0 +1,11 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("legacy-handlers.zwave-alarm-v1-lock"), + lazy_load_if_possible("legacy-handlers.schlage-lock"), + lazy_load_if_possible("legacy-handlers.samsung-lock"), + lazy_load_if_possible("legacy-handlers.keywe-lock"), +} +return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/can_handle.lua new file mode 100644 index 0000000000..324f83e6f9 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/can_handle.lua @@ -0,0 +1,10 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then + local subdriver = require("legacy-handlers.zwave-alarm-v1-lock") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/init.lua new file mode 100644 index 0000000000..5865091852 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/legacy-handlers/zwave-alarm-v1-lock/init.lua @@ -0,0 +1,146 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Alarm +local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.defaults.lockCodes +local lock_code_defaults = require "st.zwave.defaults.lockCodes" +local json = require "dkjson" + +local METHOD = { + KEYPAD = "keypad", + MANUAL = "manual", + COMMAND = "command", + AUTO = "auto" +} + +--- Default handler for alarm command class reports, these were largely OEM-defined +--- +--- This converts alarm V1 reports to correct lock events +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd st.zwave.CommandClass.Alarm.Report +local function alarm_report_handler(driver, device, cmd) + local alarm_type = cmd.args.alarm_type + local event = nil + local lock_codes = lock_code_defaults.get_lock_codes(device) + local code_id = nil + if (cmd.args.alarm_level ~= nil) then + code_id = tostring(cmd.args.alarm_level) + end + if (alarm_type == 9 or alarm_type == 17) then + event = capabilities.lock.lock.unknown() + elseif (alarm_type == 16 or alarm_type == 19) then + event = capabilities.lock.lock.unlocked() + if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + end + elseif (alarm_type == 18) then + event = capabilities.lock.lock.locked() + if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + end + elseif (alarm_type == 21) then + event = capabilities.lock.lock.locked() + if (cmd.args.alarm_level == 2) then + event["data"] = {method = METHOD.MANUAL} + else + event["data"] = {method = METHOD.KEYPAD} + end + elseif (alarm_type == 22) then + event = capabilities.lock.lock.unlocked() + event["data"] = {method = METHOD.MANUAL} + elseif (alarm_type == 23) then + event = capabilities.lock.lock.unknown() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 24) then + event = capabilities.lock.lock.locked() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 25) then + event = capabilities.lock.lock.unlocked() + event["data"] = {method = METHOD.COMMAND} + elseif (alarm_type == 26) then + event = capabilities.lock.lock.unknown() + event["data"] = {method = METHOD.AUTO} + elseif (alarm_type == 27) then + event = capabilities.lock.lock.locked() + event["data"] = {method = METHOD.AUTO} + elseif (alarm_type == 32) then + -- all user codes deleted + for code_id, _ in pairs(lock_codes) do + lock_code_defaults.code_deleted(device, code_id) + end + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + elseif (alarm_type == 33) then + -- user code deleted + if (code_id ~= nil) then + lock_code_defaults.clear_code_state(device, code_id) + if (lock_codes[code_id] ~= nil) then + lock_code_defaults.code_deleted(device, code_id) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + end + end + elseif (alarm_type == 13 or alarm_type == 112) then + -- user code changed/set + if (code_id ~= nil) then + local code_name = lock_code_defaults.get_code_name(device, code_id) + local change_type = lock_code_defaults.get_change_type(device, code_id) + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. change_type, { state_change = true }) + code_changed_event["data"] = { codeName = code_name} + lock_code_defaults.code_set_event(device, code_id, code_name) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + end + elseif (alarm_type == 34 or alarm_type == 113) then + -- duplicate lock code + if (code_id ~= nil) then + local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. lock_code_defaults.CHANGE_TYPE.FAILED, { state_change = true }) + lock_code_defaults.clear_code_state(device, code_id) + device:emit_event(code_changed_event) + end + elseif (alarm_type == 130) then + -- batteries replaced + if (device:is_cc_supported(cc.BATTERY)) then + driver:call_with_delay(10, function(d) device:send(Battery:Get({})) end ) + end + elseif (alarm_type == 161) then + -- tamper alarm + event = capabilities.tamperAlert.tamper.detected() + elseif (alarm_type == 167) then + -- low battery + if (device:is_cc_supported(cc.BATTERY)) then + driver:call_with_delay(10, function(d) device:send(Battery:Get({})) end ) + end + elseif (alarm_type == 168) then + -- critical battery + event = capabilities.battery.battery(1) + elseif (alarm_type == 169) then + -- battery too low to operate + event = capabilities.battery.battery(0) + end + + if (event ~= nil) then + device:emit_event(event) + end +end + +local zwave_lock = { + zwave_handlers = { + [cc.ALARM] = { + [Alarm.REPORT] = alarm_report_handler + } + }, + NAME = "Z-Wave lock alarm V1", + can_handle = require("legacy-handlers.zwave-alarm-v1-lock.can_handle") +} + +return zwave_lock diff --git a/drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua b/drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua new file mode 100644 index 0000000000..13d45bc4dd --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lock_handlers/capabilities.lua @@ -0,0 +1,252 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local lock_utils = require "lock_utils.utils" +local tables = require "lock_utils.tables" +local consts = require "lock_utils.constants" + +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + + +local CapabilityHandlers = {} + + +-- [[ LOCK USERS CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.add_user(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.ADD, consts.COMMAND_RESULT.BUSY) + return + end + + -- Find the smallest positive userIndex not already in the table + local next_available_index = tables.next_index(device, "users") + local status = tables.add_entry(device, "users", { + userIndex = next_available_index, + userName = command.args.userName, + userType = command.args.userType, + }) + local additional_info = status == consts.COMMAND_RESULT.SUCCESS and { userIndex = next_available_index } + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.ADD, status, additional_info) +end + +function CapabilityHandlers.update_user(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.UPDATE, consts.COMMAND_RESULT.BUSY) + return + end + + local result_status + if not tables.find_entry(device, "users", command.args.userIndex) then + result_status = tables.add_entry(device, "users", { + userIndex = command.args.userIndex, + userName = command.args.userName, + userType = command.args.userType, + }) + else + result_status = tables.update_entry(device, "users", + command.args.userIndex, + { userName = command.args.userName, userType = command.args.userType } + ) + end + + local additional_info = result_status == consts.COMMAND_RESULT.SUCCESS and { userIndex = command.args.userIndex } + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.UPDATE, result_status, additional_info) +end + +function CapabilityHandlers.delete_user(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE, consts.COMMAND_RESULT.BUSY) + return + end + + local associated_credentials = tables.find_all_entries_by(device, "credentials", "userIndex", command.args.userIndex) + for _, associated_credential in ipairs(associated_credentials) do + -- Set busy state with the full user+credential context BEFORE injecting. + -- Injected capability commands are schema-validated, so extra args like userIndex + -- would be stripped. By setting device fields here we preserve the full context. + lock_utils.set_busy_state(device, consts.LOCK_USERS.DELETE, { + userIndex = command.args.userIndex, + credentialIndex = associated_credential.credentialIndex, + credentialType = consts.CRED_TYPE_PIN, + }) + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteCredential.NAME, + args = { + credentialIndex = associated_credential.credentialIndex, + credentialType = consts.CRED_TYPE_PIN, + } + }) + end + if #associated_credentials == 0 then + -- No associated credentials: delete the user entry directly and report the result + local status = tables.delete_entry(device, "users", command.args.userIndex) + local additional_info = status == consts.COMMAND_RESULT.SUCCESS and { userIndex = command.args.userIndex } + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE, status, additional_info) + end +end + +function CapabilityHandlers.delete_all_users(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE_ALL, consts.COMMAND_RESULT.BUSY) + return + end + -- Set busy state with DELETE_ALL context BEFORE injecting so the response handler + -- knows to clear both tables and emit results for both capabilities. + lock_utils.set_busy_state(device, consts.LOCK_USERS.DELETE_ALL, {}) + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteAllCredentials.NAME, + args = { credentialType = consts.CRED_TYPE_PIN } + }) +end + + +-- [[ LOCK CREDENTIALS CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.add_credential(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.BUSY) + return + end + + -- A userIndex of 0 means "auto-assign the next available slot" + local user_index = command.args.userIndex == 0 and tables.next_index(device, "users") or command.args.userIndex + local cred_index = command.args.userIndex == 0 and tables.next_index(device, "credentials") or command.args.userIndex + + if #(tables.get_state(device, "credentials") or {}) >= tables.get_max_entries(device, "credentials") then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.RESOURCE_EXHAUSTED) + return + end + + -- Set busy state and attempt to set the PIN on-device. + lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.ADD, { + userIndex = user_index, + credentialIndex = cred_index, + credentialType = command.args.credentialType, + credentialName = command.args.credentialName, + }) + device:send(UserCode:Set({ + user_identifier = cred_index, + user_code = command.args.credentialData, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + })) +end + +function CapabilityHandlers.update_credential(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.BUSY) + return + end + + if not tables.find_entry(device, "credentials", command.args.credentialIndex) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.FAILURE) + return + end + + lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.UPDATE, { + userIndex = command.args.userIndex, + credentialIndex = command.args.credentialIndex, + credentialType = command.args.credentialType, + credentialName = command.args.credentialName, + }) + device:send(UserCode:Set({ + user_identifier = command.args.credentialIndex, + user_code = command.args.credentialData, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + })) +end + +function CapabilityHandlers.delete_credential(driver, device, command) + local cmd_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + + if cmd_in_progress == consts.LOCK_USERS.DELETE then + -- Injected by deleteUser; busy state was already set with the full LOCK_USERS.DELETE context. + local credential_args = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) or {} + device:send(UserCode:Set({ + user_identifier = credential_args.credentialIndex, + user_id_status = UserCode.user_id_status.AVAILABLE + })) + elseif lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, consts.COMMAND_RESULT.BUSY) + else + -- Standalone deleteCredential: look up the credential to obtain its associated userIndex. + local found_cred = tables.find_entry(device, "credentials", command.args.credentialIndex) + if not found_cred then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, consts.COMMAND_RESULT.FAILURE) + return + end + lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.DELETE, { + credentialIndex = command.args.credentialIndex, + credentialType = command.args.credentialType, + userIndex = found_cred.userIndex, + }) + device:send(UserCode:Set({ + user_identifier = command.args.credentialIndex, + user_id_status = UserCode.user_id_status.AVAILABLE + })) + end +end + +function CapabilityHandlers.delete_all_credentials(driver, device, command) + local cmd_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + + local send_clear_commands = function() + local credentials = tables.get_state(device, "credentials") or {} + local delay = 0 + for _, credential in ipairs(credentials) do + device.thread:call_with_delay(delay, function() + device:send(UserCode:Set({ + user_identifier = credential.credentialIndex, + user_id_status = UserCode.user_id_status.AVAILABLE + })) + end) + delay = delay + 2 + end + -- Delayed cleanup: ensure all tables are cleared after individual responses complete + device.thread:call_with_delay(delay + 4, function() + if cmd_in_progress == consts.LOCK_USERS.DELETE_ALL then + tables.delete_all_entries(device, "users") + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE_ALL, consts.COMMAND_RESULT.SUCCESS) + end + tables.delete_all_entries(device, "credentials") + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE_ALL, consts.COMMAND_RESULT.SUCCESS) + lock_utils.clear_busy_state(device) + end) + end + + if cmd_in_progress == consts.LOCK_USERS.DELETE_ALL then + -- Injected by deleteAllUsers; busy state was already set with LOCK_USERS.DELETE_ALL context. + send_clear_commands() + elseif lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE_ALL, consts.COMMAND_RESULT.BUSY) + else + lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.DELETE_ALL, command.args) + send_clear_commands() + end +end + + +-- [[ REFRESH CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.refresh(driver, device, cmd) + device:send(DoorLock:OperationGet({})) + device:send(Battery:Get({})) + + if device:supports_capability(capabilities.lockCredentials) then + -- If we are missing the cached values for these attributes, read them so we can properly manage them locally + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME) == nil) or + (device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME) == nil) then + device:send(UserCode:UsersNumberGet({})) + end + end +end + +return CapabilityHandlers diff --git a/drivers/SmartThings/zwave-lock/src/lock_handlers/zwave_responses.lua b/drivers/SmartThings/zwave-lock/src/lock_handlers/zwave_responses.lua new file mode 100644 index 0000000000..cfabe41932 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lock_handlers/zwave_responses.lua @@ -0,0 +1,243 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local socket = require "cosock.socket" + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local TamperDefaults = require "st.zwave.defaults.tamperAlert" + +local consts = require "lock_utils.constants" +local lock_utils = require "lock_utils.utils" +local tables = require "lock_utils.tables" + + +local ZwaveHandlers = {} + + +-- [[ USER CODE COMMAND CLASS ]] + +function ZwaveHandlers.user_code_report(driver, device, cmd) + -- zw report values + local credential_index = cmd.args.user_identifier + local user_id_status = cmd.args.user_id_status + + -- cached values from capability command + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + + -- Determine boolean states of user code slot + local user_code_occupied = user_id_status == UserCode.user_id_status.ENABLED_GRANT_ACCESS or + (user_id_status == UserCode.user_id_status.STATUS_NOT_AVAILABLE and cmd.args.user_code) + local user_code_available = user_id_status == UserCode.user_id_status.AVAILABLE + + if command_in_progress == consts.SYNC.CODES_FROM_LOCK then + if user_code_occupied then + -- If an entry already exists at this index, this will be a no-op. + tables.add_entry(device, "users", { + userIndex = credential_index, + userName = "Guest " .. credential_index, + userType = "guest", + }) + tables.add_entry(device, "credentials", { + userIndex = credential_index, + credentialIndex = credential_index, + credentialType = consts.CRED_TYPE_PIN, + credentialName = "Guest " .. credential_index, + }) + -- reset the consecutive unoccupied codes counter since we found an occupied code + device:set_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES, 0) + elseif user_code_available then + local consecutive_unoccupied_codes = (device:get_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES) or 0) + 1 + device:set_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES, consecutive_unoccupied_codes) + end + + -- Sync: continue to next code or finish + if credential_index >= tables.get_max_entries(device, "credentials") or + (device:get_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES) or 0) > 5 then + -- stop if we hit the max number of codes supported by the lock, or if we have received 5 consecutive unoccupied codes. + device:set_field(consts.SYNC.CODE_INDEX, nil) + lock_utils.clear_busy_state(device) + else + local synced_code_index = device:get_field(consts.SYNC.CODE_INDEX) + 1 + device:set_field(consts.SYNC.CODE_INDEX, synced_code_index) + lock_utils.set_busy_state(device, consts.SYNC.CODES_FROM_LOCK, { checkingCode = synced_code_index }) + device:send(UserCode:Get({user_identifier = synced_code_index})) + end + return + elseif user_code_occupied then + -- Code slot is now occupied: add or update credential and associated user + lock_utils.set_credential_report_helper(device, credential_index) + elseif user_code_available then + if command_in_progress == consts.LOCK_CREDENTIALS.ADD then + lock_utils.emit_command_result(device, capabilities.lockCredentials, + consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.FAILURE) + lock_utils.clear_busy_state(device) + elseif command_in_progress == consts.LOCK_CREDENTIALS.DELETE or tables.find_entry(device, "credentials", credential_index) then + lock_utils.delete_credential_report_helper(device, credential_index) + end + end +end + +function ZwaveHandlers.users_number_report(driver, device, cmd) + device:emit_event(capabilities.lockUsers.totalUsersSupported( + cmd.args.supported_users, { state_change = true, visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.pinUsersSupported( + cmd.args.supported_users, { state_change = true, visibility = { displayed = false } })) +end + + +-- [[ NOTIFICATION COMMAND CLASS ]] -- + +function ZwaveHandlers.user_code_event_handler(driver, device, cmd) + if cmd.args.notification_type ~= Notification.notification_type.ACCESS_CONTROL then + return + end + -- zw event values + local event = cmd.args.event + local credential_index = tonumber(lock_utils.get_code_id_from_notification_event( + cmd.args.event_parameter, cmd.args.v1_alarm_level)) + -- cached value from capability event, if applicable + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + -- This type includes many lock-related events, way too many to list, + -- including user code changes and door operations + local access_control_event = Notification.event.access_control + + if event == access_control_event.ALL_USER_CODES_DELETED then + tables.delete_all_entries(device, "credentials") + tables.delete_all_entries(device, "users") + + elseif event == access_control_event.SINGLE_USER_CODE_DELETED then + lock_utils.delete_credential_report_helper(device, credential_index) + + elseif event == access_control_event.NEW_USER_CODE_ADDED then + lock_utils.set_credential_report_helper(device, credential_index) + + elseif event == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then + -- aka "master code" changed + if command_in_progress == consts.LOCK_CREDENTIALS.UPDATE then + lock_utils.emit_command_result(device, capabilities.lockCredentials, + consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.SUCCESS) + lock_utils.clear_busy_state(device) + end + + elseif event == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then + if command_in_progress then + lock_utils.emit_command_result(device, capabilities.lockCredentials, + consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.DUPLICATE) + lock_utils.clear_busy_state(device) + end + end +end + +function ZwaveHandlers.door_operation_event_handler(driver, device, cmd) + if cmd.args.notification_type ~= Notification.notification_type.ACCESS_CONTROL then + return + end + -- zw event value + local event = cmd.args.event + -- This type includes many lock-related events, way too many to list, + -- including user code changes and door operations + local access_control_event = Notification.event.access_control + -- send lock, unlock, or unknown event based on the event coded + local capability_event + if not (event >= access_control_event.MANUAL_LOCK_OPERATION and event <= access_control_event.LOCK_JAMMED) then + return -- This is the subset of event kinds that we care about for door operation reporting + elseif ((event >= access_control_event.MANUAL_LOCK_OPERATION and + event <= access_control_event.KEYPAD_UNLOCK_OPERATION) or + event == access_control_event.AUTO_LOCK_LOCKED_OPERATION) then + -- even event codes are unlocks, odd event codes are locks + local events = {[0] = capabilities.lock.lock.unlocked(), [1] = capabilities.lock.lock.locked()} + capability_event = events[event & 1] + elseif (event >= access_control_event.MANUAL_NOT_FULLY_LOCKED_OPERATION and + event <= access_control_event.LOCK_JAMMED) then + capability_event = capabilities.lock.lock.unknown() + else + return -- no lock event to send for this code + end + + local access_control_event_capability_map = { + [access_control_event.MANUAL_UNLOCK_OPERATION] = "manual", + [access_control_event.MANUAL_LOCK_OPERATION] = "manual", + [access_control_event.MANUAL_NOT_FULLY_LOCKED_OPERATION] = "manual", + [access_control_event.RF_LOCK_OPERATION] = "command", + [access_control_event.RF_UNLOCK_OPERATION] = "command", + [access_control_event.RF_NOT_FULLY_LOCKED_OPERATION] = "command", + [access_control_event.KEYPAD_LOCK_OPERATION] = "keypad", + [access_control_event.KEYPAD_UNLOCK_OPERATION] = "keypad", + [access_control_event.AUTO_LOCK_LOCKED_OPERATION] = "auto", + [access_control_event.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION] = "auto" + } + + capability_event.data = {} + capability_event.data.method = access_control_event_capability_map[event] + + if (event == access_control_event.MANUAL_UNLOCK_OPERATION and cmd.args.event_parameter == 2) then + capability_event.data.method = "keypad" -- some locks can distinguish being manually locked via keypad + elseif (event == access_control_event.KEYPAD_LOCK_OPERATION or event == access_control_event.KEYPAD_UNLOCK_OPERATION) then + local code_id = tonumber(lock_utils.get_code_id_from_notification_event( + cmd.args.event_parameter, cmd.args.v1_alarm_level)) -- Look up stored lockUsers data if applicable + if device:supports_capability(capabilities.lockUsers) then + local credential = tables.find_entry(device, "credentials", code_id) + if credential then + local user = tables.find_entry(device, "users", credential.userIndex) + capability_event.data.userIndex = credential.userIndex + if user then + capability_event.data.userName = user.userName + capability_event.data.userType = user.userType + end + else + capability_event.data.userIndex = code_id + end + end + end + + -- Delay timer logic to handle duplicate lock state reports + if device:get_latest_state( + "main", + capabilities.lock.ID, + capabilities.lock.lock.ID) == capability_event.value.value then + local preceding_event_time = device:get_field(consts.DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < consts.MAX_DELAY then + device:set_field(consts.DELAY_LOCK_EVENT, time_diff) + end + end + + local timer = device:get_field(consts.DELAY_LOCK_EVENT_TIMER) + if timer ~= nil then + device.thread:cancel_timer(timer) + device:set_field(consts.DELAY_LOCK_EVENT_TIMER, nil) + end + + device:emit_event(capability_event) +end + +function ZwaveHandlers.notification_report(driver, device, cmd) + ZwaveHandlers.user_code_event_handler(driver, device, cmd) + ZwaveHandlers.door_operation_event_handler(driver, device, cmd) + -- Tamper events handled by default tamper handler + TamperDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](driver, device, cmd) +end + + +-- [[ TIME COMMAND CLASS ]] -- + +function ZwaveHandlers.time_get_handler(driver, device, cmd) + local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) + local time = os.date("*t") + device:send_to_component( + Time:Report({ + hour_local_time = time.hour, + minute_local_time = time.min, + second_local_time = time.sec + }), + device:endpoint_to_component(cmd.src_channel) + ) +end + +return ZwaveHandlers diff --git a/drivers/SmartThings/zwave-lock/src/lock_utils/constants.lua b/drivers/SmartThings/zwave-lock/src/lock_utils/constants.lua new file mode 100644 index 0000000000..f66c452beb --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lock_utils/constants.lua @@ -0,0 +1,48 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lock_constants = {} + +lock_constants.DRIVER_STATE = { + BUSY = "busy", + COMMAND_IN_PROGRESS = "commandInProgress", + CREDENTIAL_ARGS_IN_USE = "currentCredential", + SLGA_MIGRATED = "slgaMigrated", +} + +lock_constants.SYNC = { + CODES_FROM_LOCK = "syncCodesFromLock", + CODE_INDEX = "syncCodeIndex", + CONSECUTIVE_UNOCCUPIED_CODES = "consecutiveUnoccupiedCodes", +} + +lock_constants.COMMAND_RESULT = { + SUCCESS = "success", + FAILURE = "failure", + DUPLICATE = "duplicate", + OCCUPIED = "occupied", + INVALID_COMMAND = "invalidCommand", + RESOURCE_EXHAUSTED = "resourceExhausted", + BUSY = "busy" +} + +lock_constants.LOCK_CREDENTIALS = { + ADD = "addCredential", + UPDATE = "updateCredential", + DELETE = "deleteCredential", + DELETE_ALL = "deleteAllCredentials" +} + +lock_constants.LOCK_USERS = { + ADD = "addUser", + UPDATE = "updateUser", + DELETE = "deleteUser", + DELETE_ALL = "deleteAllUsers" +} + +lock_constants.CRED_TYPE_PIN = "pin" +lock_constants.DELAY_LOCK_EVENT = "_delay_lock_event" +lock_constants.DELAY_LOCK_EVENT_TIMER = "_delay_lock_event_timer" +lock_constants.MAX_DELAY = 10 + +return lock_constants diff --git a/drivers/SmartThings/zwave-lock/src/lock_utils/tables.lua b/drivers/SmartThings/zwave-lock/src/lock_utils/tables.lua new file mode 100644 index 0000000000..bfe3562344 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lock_utils/tables.lua @@ -0,0 +1,268 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local st_utils = require "st.utils" +local COMMAND_RESULT = require "lock_utils.constants".COMMAND_RESULT + +local table_utils = {} + +-- DEFS describes how each capability-backed state table is structured: +-- +-- capability SmartThings capability (used for device support checks and to get latest state) +-- attribute Capability attribute function (used to emit state) +-- max_entries Attribute of the capability that defines the maximum number of entries allowed in the table +-- match_key Key used to identify entries in flat tables +-- required_keys Keys that must be non-nil when adding an entry +-- persistent_field device:set_field key used to back up the table across restarts +-- + +local DEFS = { + users = { + capability = capabilities.lockUsers, + attribute = capabilities.lockUsers.users, + max_entries = capabilities.lockUsers.totalUsersSupported, + match_key = "userIndex", + required_keys = {"userIndex", "userType"}, + persistent_field = "persistedUsers", + }, + credentials = { + capability = capabilities.lockCredentials, + attribute = capabilities.lockCredentials.credentials, + max_entries = capabilities.lockCredentials.pinUsersSupported, + match_key = "credentialIndex", + required_keys = {"userIndex", "credentialIndex", "credentialType"}, + persistent_field = "persistedCredentials", + } +} + +-- Resolve a table name to its definition. Logs an error and returns nil if unknown. +local function resolve_table_def(device, table_name) + local def = DEFS[table_name] + if not def then + device.log.error(string.format("table_helpers: unknown table %q", table_name)) + end + return def +end + +-- Validate that an entry table contains all required keys. +local function validate_entry(device, entry, required_keys) + for _, key in ipairs(required_keys or {}) do + if entry[key] == nil then + device.log.error(string.format("table_helpers: entry missing required key %q", key)) + return false + end + end + return true +end + +-- Write the current table contents to the device's persistent field store so that +-- the state survives driver restarts and can be restored if the capability state +-- cache is wiped. +local function persist_table(device, def, data) + device:set_field(def.persistent_field, st_utils.deep_copy(data), { persist = true }) +end + +-- Read the current state for a table and return a deep-copied array. +-- Accepts either a string table name ("users", "credentials") or a DEFS entry directly. +-- Returns nil (with a warning) if the capability is unsupported by the device. +-- When the capability state cache has been wiped (get_latest_state returns nil), +-- falls back to the persistent field store so that callers always receive the +-- last-known state rather than an empty table. +--- @return table[] | nil +function table_utils.get_state(device, name_or_def) + local def = type(name_or_def) == "string" and resolve_table_def(device, name_or_def) or name_or_def + if not def then return nil end + if not device:supports_capability(def.capability, "main") then + device.log.warn(string.format( + "table_helpers: device does not support capability %q", def.capability.ID + )) + return + end + local state = device:get_latest_state("main", def.capability.ID, def.attribute.NAME) + if state ~= nil then + return st_utils.deep_copy(state) + end + -- Capability state cache is absent (e.g. after a hub reboot); fall back to the + -- persistent store so that callers see the last-known table contents. + return st_utils.deep_copy(device:get_field(def.persistent_field) or {}) +end + +-- Find an entry in a named table where the match_key equals value. +-- Returns the matching entry, or nil if not found. +function table_utils.find_entry(device, table_name, value) + local def = resolve_table_def(device, table_name) + if not def then return nil end + local t = table_utils.get_state(device, def) + if not t then return nil end + for _, entry in ipairs(t) do + if entry[def.match_key] == value then return entry end + end + return nil +end + +-- Find an entry in a named table where entry[key] equals value (arbitrary key search). +-- Returns the matching entry, or nil if not found. +function table_utils.find_entry_by(device, table_name, key, value) + local def = resolve_table_def(device, table_name) + if not def then return nil end + local t = table_utils.get_state(device, def) + if not t then return nil end + for _, entry in ipairs(t) do + if entry[key] == value then return entry end + end + return nil +end + +-- Find all entries in a named table where entry[key] equals value (arbitrary key search). +-- Returns an array of matching entries, or an empty array if none found. +function table_utils.find_all_entries_by(device, table_name, key, value) + local def = resolve_table_def(device, table_name) + if not def then return {} end + local t = table_utils.get_state(device, def) + if not t then return {} end + local matches = {} + for _, entry in ipairs(t) do + if entry[key] == value then table.insert(matches, entry) end + end + return matches +end + +-- Return the lowest positive integer not yet used as the match_key in the named table. +-- Used to auto-assign the next available slot for a new entry. +function table_utils.next_index(device, table_name) + local def = resolve_table_def(device, table_name) + if not def then return 1 end + local t = table_utils.get_state(device, def) or {} + local occupied = {} + for _, entry in ipairs(t) do occupied[entry[def.match_key]] = true end + local idx = 1 + while occupied[idx] do idx = idx + 1 end + return idx +end + +function table_utils.get_max_entries(device, table_name) + local def = resolve_table_def(device, table_name) + if not def then return end + return device:get_latest_state("main", def.capability.ID, def.max_entries.NAME, 20) -- arbitrary, default to 20 if the attribute is missing +end + +-- Add an entry to a named table. The entry must satisfy all required_keys for +-- that table. An entry whose match_key value already exists in the +-- table is skipped to prevent duplicates. If the table has a max_entries limit, +-- entries that exceed the limit are not added. +function table_utils.add_entry(device, table_name, entry) + device.log.debug("table_helpers: attempting to add entry " .. st_utils.stringify_table(entry) .. " to " .. table_name) + local def = resolve_table_def(device, table_name) + if not def then return COMMAND_RESULT.FAILURE end + if not validate_entry(device, entry, def.required_keys) then return COMMAND_RESULT.FAILURE end + local t = table_utils.get_state(device, def) + if not t then return COMMAND_RESULT.FAILURE end + + if #t >= table_utils.get_max_entries(device, table_name) then + device.log.warn(string.format( + "table_helpers: cannot add entry to %q, max entries reached", table_name + )) + return COMMAND_RESULT.RESOURCE_EXHAUSTED + end + + -- Object entry: skip if an entry with the same match_key value already exists + if def.match_key then + for _, existing in ipairs(t) do + if existing[def.match_key] == entry[def.match_key] then + device.log.warn(string.format( + "table_helpers: entry with %s == %s already exists in %q, skipping", + def.match_key, tostring(entry[def.match_key]), table_name + )) + return COMMAND_RESULT.OCCUPIED + end + end + end + + table.insert(t, st_utils.deep_copy(entry)) + + device:emit_event(def.attribute(t, {visibility = {displayed = false}})) + persist_table(device, def, t) + return COMMAND_RESULT.SUCCESS +end + + +--- Update fields of an existing entry in a table. +--- The entry to update is identified by the match_key parameter in DEFS. +function table_utils.update_entry(device, table_name, match_value, updates) + device.log.debug("table_helpers: attempting to update entry " .. match_value .. " in " .. table_name .. ": " .. st_utils.stringify_table(updates)) + local def = resolve_table_def(device, table_name) + if not def then return COMMAND_RESULT.FAILURE end + local t = table_utils.get_state(device, def) + if not t then return COMMAND_RESULT.FAILURE end + + for _, entry in ipairs(t) do + if entry[def.match_key] == match_value then + for k, v in pairs(updates) do + entry[k] = v + end + device:emit_event(def.attribute(t, {visibility = {displayed = false}})) + persist_table(device, def, t) + return COMMAND_RESULT.SUCCESS + end + end + + device.log.warn(string.format( + "table_helpers: no entry found in %q with %s == %s", + table_name, def.match_key, tostring(match_value) + )) + return COMMAND_RESULT.FAILURE +end + + +-- Delete an entry from a table. +-- +-- Returns SUCCESS, or FAILURE if nothing matched. +function table_utils.delete_entry(device, table_name, matcher) + device.log.debug("table_helpers: attempting to delete entry " .. matcher .. " from " .. table_name) + local def = resolve_table_def(device, table_name) + if not def then return COMMAND_RESULT.FAILURE end + local t = table_utils.get_state(device, def) + if not t then return COMMAND_RESULT.FAILURE end + + local predicate = function(entry) return entry[def.match_key] == matcher end + + for i, entry in ipairs(t) do + if predicate(entry) then + table.remove(t, i) + device:emit_event(def.attribute(t, {visibility = {displayed = false}})) + persist_table(device, def, t) + return COMMAND_RESULT.SUCCESS + end + end + return COMMAND_RESULT.FAILURE +end + +-- Delete all entries from a table. +function table_utils.delete_all_entries(device, table_name) + device.log.debug("table_helpers: attempting to delete all entries from " .. table_name) + local def = resolve_table_def(device, table_name) + if not def then return COMMAND_RESULT.FAILURE end + device:emit_event(def.attribute({}, {visibility = {displayed = false}})) + persist_table(device, def, {}) + return COMMAND_RESULT.SUCCESS +end + +-- Restore capability state from the persistent field store. +-- Called during init to re-emit table events if the capability state cache +-- has been wiped (e.g. after a hub reboot). Only emits for tables that have +-- persisted data, are in a nil state, and whose capability is supported by the device. +function table_utils.restore_from_persistent_store(device) + for _, internal in pairs(DEFS) do + if device:supports_capability(internal.capability, "main") and + device:get_latest_state("main", internal.capability.ID, internal.attribute.NAME) == nil + then + local persisted = st_utils.deep_copy(device:get_field(internal.persistent_field)) + if persisted and #persisted > 0 then + device:emit_event(internal.attribute(persisted, {visibility = {displayed = false}})) + end + end + end +end + +return table_utils diff --git a/drivers/SmartThings/zwave-lock/src/lock_utils/utils.lua b/drivers/SmartThings/zwave-lock/src/lock_utils/utils.lua new file mode 100644 index 0000000000..64851e485a --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/lock_utils/utils.lua @@ -0,0 +1,160 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local consts = require "lock_utils.constants" +local tables = require "lock_utils.tables" + +local lock_utils = {} + +-- [[ BUSY STATE MANAGEMENT ]] -- + +-- Check if we are currently busy performing a task, or at least 10 seconds have passed since the busy state was last set. +-- If busy, return true. If not busy, clear any stale state and return false. +function lock_utils.is_device_busy(device) + local c_time = os.time() + local busy_since = device:get_field(consts.DRIVER_STATE.BUSY) or false + + if (busy_since == false) or (c_time - busy_since > 10) then + lock_utils.clear_busy_state(device) + return false + end + return true +end + +-- Set states that may be required when in busy state +function lock_utils.set_busy_state(device, command_name, command_args) + device:set_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS, command_name) + device:set_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, command_args or {}) + device:set_field(consts.DRIVER_STATE.BUSY, os.time()) +end + +-- Clear states that were set when in busy state +function lock_utils.clear_busy_state(device) + device:set_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS, nil) + device:set_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, nil) + device:set_field(consts.DRIVER_STATE.BUSY, false) +end + + +-- [[ SYNC STATE MANAGEMENT ]] -- + +function lock_utils.sync_device_state(device) + local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + + if (device:get_field(consts.SYNC.CODE_INDEX) == nil) then + device:set_field(consts.SYNC.CODE_INDEX, 1) + end + lock_utils.set_busy_state(device, consts.SYNC.CODES_FROM_LOCK) + device:send(UserCode:Get({user_identifier = device:get_field(consts.SYNC.CODE_INDEX)})) +end + + +-- [[ COMMAND RESULT STATE MANAGEMENT ]] -- + +function lock_utils.emit_command_result(device, capability, command_name, status_code, additional_info) + local info = additional_info or {} + info.commandName = command_name + info.statusCode = status_code + if capability then + device:emit_event(capability.commandResult(info, {state_change = true, visibility = {displayed = false}})) + end +end + + +-- [[ HELPERS ]] -- + +function lock_utils.get_code_id_from_notification_event(event_params, v1_alarm_level) + local code_id = v1_alarm_level + if event_params ~= nil and event_params ~= "" then + event_params = {event_params:byte(1,-1)} + code_id = (#event_params == 1) and event_params[1] or event_params[3] + end + return tostring(code_id) +end + +function lock_utils.set_credential_report_helper(device, credential_index) + -- cached values from capability command + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + local credential_args = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) or {} + + local result_status + if command_in_progress == consts.LOCK_CREDENTIALS.ADD then + result_status = tables.add_entry(device, "credentials", { + userIndex = credential_args.userIndex, + credentialIndex = credential_args.credentialIndex, + credentialType = consts.CRED_TYPE_PIN, + credentialName = credential_args.credentialName, + }) + elseif command_in_progress == consts.LOCK_CREDENTIALS.UPDATE then + result_status = consts.COMMAND_RESULT.SUCCESS + elseif not tables.find_entry(device, "credentials", credential_index) then + -- credential does not exist and no add/update command in progress, therefore it was added out-of-band + -- check what user slot we have to associate this with, and add a default user + local next_available_index = tables.next_index(device, "users") + if next_available_index <= tables.get_max_entries(device, "users") then + tables.add_entry(device, "users", { + userIndex = next_available_index, + userName = "Guest " .. next_available_index, + userType = "guest", + }) + tables.add_entry(device, "credentials", { + userIndex = next_available_index, + credentialIndex = credential_index, + credentialType = consts.CRED_TYPE_PIN, + credentialName = "Guest " .. next_available_index, + }) + end + end + + -- emit command result + if command_in_progress and result_status then + local additional_info = result_status == consts.COMMAND_RESULT.SUCCESS and { + userIndex = credential_args.userIndex, + credentialIndex = credential_args.credentialIndex, + } or nil + lock_utils.emit_command_result(device, capabilities.lockCredentials, command_in_progress, result_status, additional_info) + lock_utils.clear_busy_state(device) + end +end + +function lock_utils.delete_credential_report_helper(device, credential_index) + -- cached values from capability command + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + local credential_args_in_use = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) + + local user_status, credential_status + if command_in_progress == consts.LOCK_USERS.DELETE then + user_status = tables.delete_entry(device, "users", credential_args_in_use.userIndex) + credential_status = tables.delete_entry(device, "credentials", credential_args_in_use.credentialIndex) + elseif command_in_progress == consts.LOCK_CREDENTIALS.DELETE then + credential_status = tables.delete_entry(device, "credentials", credential_args_in_use.credentialIndex) + else + -- out-of-band deletion, find the credential index and associated user to delete + local credential = tables.find_entry(device, "credentials", credential_index) + if credential then + credential_status = tables.delete_entry(device, "credentials", credential_index) + local associated_user = tables.find_entry_by(device, "users", "userIndex", credential.userIndex) + if associated_user then + tables.delete_entry(device, "users", credential.userIndex) + end + end + end + + -- emit command results + if command_in_progress == consts.LOCK_USERS.DELETE and user_status and credential_status then + -- the deleteUser command injects a deleteCredential command, so both command results should be emitted in this case. + local user_info = user_status == consts.COMMAND_RESULT.SUCCESS and { userIndex = credential_args_in_use.userIndex } or nil + local cred_info = credential_status == consts.COMMAND_RESULT.SUCCESS and { credentialIndex = credential_args_in_use.credentialIndex, userIndex = credential_args_in_use.userIndex } or nil + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE, user_status, user_info) + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, credential_status, cred_info) + lock_utils.clear_busy_state(device) + elseif command_in_progress == consts.LOCK_CREDENTIALS.DELETE and credential_status then + local cred_info = credential_status == consts.COMMAND_RESULT.SUCCESS and { credentialIndex = credential_args_in_use.credentialIndex, userIndex = credential_args_in_use.userIndex } or nil + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, credential_status, cred_info) + lock_utils.clear_busy_state(device) + end +end + + +return lock_utils diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua index e9222cb8fb..002d4c169d 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/can_handle.lua @@ -1,12 +1,15 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function can_handle_samsung_lock(opts, self, device, cmd, ...) - local SAMSUNG_MFR = 0x022E - if device.zwave_manufacturer_id == SAMSUNG_MFR then - return true, require("samsung-lock") +return function(opts, driver, device, cmd) + local consts = require("lock_utils.constants") + local slga_migrated = device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) + if slga_migrated then + local SAMSUNG_MFR = 0x022E + if device.zwave_manufacturer_id == SAMSUNG_MFR then + local subdriver = require("samsung-lock") + return true, subdriver + end end return false end - -return can_handle_samsung_lock diff --git a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua index b2f4f60975..80c3ce25d5 100644 --- a/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/samsung-lock/init.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" @@ -9,28 +8,9 @@ local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) local access_control_event = Notification.event.access_control -local json = require "dkjson" -local constants = require "st.zwave.constants" - -local LockDefaults = require "st.zwave.defaults.lock" -local LockCodesDefaults = require "st.zwave.defaults.lockCodes" -local get_lock_codes = LockCodesDefaults.get_lock_codes -local clear_code_state = LockCodesDefaults.clear_code_state -local code_deleted = LockCodesDefaults.code_deleted - - -local function get_ongoing_code_set(device) - local code_id - local code_state = device:get_field(constants.CODE_STATE) - if code_state ~= nil then - for key, state in pairs(code_state) do - if state ~= nil then - code_id = key:match("setName(%d)") - end - end - end - return code_id -end +local consts = require "lock_utils.constants" +local tables = require "lock_utils.tables" +local zwave_handlers = require "lock_handlers.zwave_responses" local function notification_report_handler(self, device, cmd) local event @@ -39,36 +19,25 @@ local function notification_report_handler(self, device, cmd) if event_code == access_control_event.AUTO_LOCK_NOT_FULLY_LOCKED_OPERATION then event = capabilities.lock.lock.unlocked() elseif event_code == access_control_event.NEW_USER_CODE_ADDED then - local code_id = get_ongoing_code_set(device) - if code_id ~= nil then - device:send(UserCode:Get({user_identifier = code_id})) + local credential_args = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + if command_in_progress == consts.LOCK_CREDENTIALS.ADD and credential_args ~= nil then + device:send(UserCode:Get({ user_identifier = credential_args.credentialIndex })) return end - elseif event_code == access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE then - local code_id = get_ongoing_code_set(device) - if code_id ~= nil then - event = capabilities.lockCodes.codeChanged(code_id .. " failed", { state_change = true }) - clear_code_state(device, code_id) - end elseif event_code == access_control_event.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION then - -- Update Master Code in the same way as in defaults... - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) - -- ...and delete rest of them, as lock does - local lock_codes = get_lock_codes(device) - for code_id, _ in pairs(lock_codes) do - if code_id ~= "0" then - code_deleted(device, code_id) - end - end - event = capabilities.lockCodes.lockCodes(json.encode(get_lock_codes(device)), { visibility = { displayed = false } }) + -- All other codes are deleted when the master code is changed + tables.delete_all_entries(device, "credentials") + tables.delete_all_entries(device, "users") + return end end if event ~= nil then device:emit_event(event) else - LockDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, cmd) + zwave_handlers.door_operation_event_handler(self, device, cmd) + zwave_handlers.code_event_handler(self, device, cmd) end end @@ -77,7 +46,6 @@ local function do_configure(self, device) -- taken directly from DTH -- Samsung locks won't allow you to enter the pairing menu when locked, so it must be unlocked device:emit_event(capabilities.lock.lock.unlocked()) - device:emit_event(capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } })) end local samsung_lock = { diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua index e9f3cfb84c..d57135920d 100644 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/can_handle.lua @@ -1,12 +1,15 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function can_handle_schlage_lock(opts, self, device, cmd, ...) - local SCHLAGE_MFR = 0x003B - if device.zwave_manufacturer_id == SCHLAGE_MFR then - return true, require("schlage-lock") +return function(opts, driver, device, cmd) + local consts = require("lock_utils.constants") + local slga_migrated = device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) + if slga_migrated then + local SCHLAGE_MFR = 0x003B + if device.zwave_manufacturer_id == SCHLAGE_MFR then + local subdriver = require("schlage-lock") + return true, subdriver + end end return false end - -return can_handle_schlage_lock diff --git a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua index 6b22049beb..ee04fe42a0 100644 --- a/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/schlage-lock/init.lua @@ -1,77 +1,22 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local capabilities = require "st.capabilities" local cc = require "st.zwave.CommandClass" local constants = require "st.zwave.constants" -local json = require "dkjson" local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) local user_id_status = UserCode.user_id_status -local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local access_control_event = Notification.event.access_control local Configuration = (require "st.zwave.CommandClass.Configuration")({version=2}) local Basic = (require "st.zwave.CommandClass.Basic")({version=1}) local Association = (require "st.zwave.CommandClass.Association")({version=1}) -local LockCodesDefaults = require "st.zwave.defaults.lockCodes" +local tables = require "lock_utils.tables" +local zwave_handlers = require "lock_handlers.zwave_responses" +local cap_handlers = require "lock_handlers.capabilities" local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} -local DEFAULT_COMMANDS_DELAY = 4.2 -- seconds - -local function set_code_length(self, device, cmd) - local length = cmd.args.length - if length >= 4 and length <= 8 then - device:send(Configuration:Set({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = length, - size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size - })) - end -end - -local function reload_all_codes(self, device, cmd) - LockCodesDefaults.capability_handlers[capabilities.lockCodes.commands.reloadAllCodes](self, device, cmd) - local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - if current_code_length == nil then - device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) - end -end - -local function set_code(self, device, cmd) - if (cmd.args.codePIN == "") then - self:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.nameSlot.NAME, - args = {cmd.args.codeSlot, cmd.args.codeName}, - }) - else - -- copied from defaults with additional check for Schlage's configuration - if (cmd.args.codeName ~= nil and cmd.args.codeName ~= "") then - if (device:get_field(constants.CODE_STATE) == nil) then device:set_field(constants.CODE_STATE, { persist = true }) end - local code_state = device:get_field(constants.CODE_STATE) - code_state["setName"..cmd.args.codeSlot] = cmd.args.codeName - device:set_field(constants.CODE_STATE, code_state, { persist = true }) - end - local send_set_user_code = function () - device:send(UserCode:Set({ - user_identifier = cmd.args.codeSlot, - user_code = cmd.args.codePIN, - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - end - local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) - if current_code_length == nil then - device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) - device.thread:call_with_delay(DEFAULT_COMMANDS_DELAY, send_set_user_code) - else - send_set_user_code() - end - end -end - local function do_configure(self, device) device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) device:send(Association:Set({grouping_identifier = 2, node_ids = {self.environment_info.hub_zwave_id}})) @@ -86,15 +31,14 @@ local function configuration_report(self, device, cmd) local parameter_number = cmd.args.parameter_number if parameter_number == SCHLAGE_LOCK_CODE_LENGTH_PARAM.number then local reported_code_length = cmd.args.configuration_value - local current_code_length = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.codeLength.NAME) + local current_code_length = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) if current_code_length ~= nil and current_code_length ~= reported_code_length then - local all_codes_deleted_mocked_command = Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = access_control_event.ALL_USER_CODES_DELETED - }) - LockCodesDefaults.zwave_handlers[cc.NOTIFICATION][Notification.REPORT](self, device, all_codes_deleted_mocked_command) + -- when the code length is changed, all the codes have been wiped + tables.delete_all_entries(device, "credentials") + tables.delete_all_entries(device, "users") end - device:emit_event(capabilities.lockCodes.codeLength(reported_code_length)) + device:emit_event(capabilities.lockCredentials.minPinCodeLen(reported_code_length)) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(reported_code_length)) end end @@ -114,49 +58,35 @@ local function is_user_code_report_mfr_specific(device, cmd) end local function user_code_report_handler(self, device, cmd) - local code_id = cmd.args.user_identifier + local credential_index = cmd.args.user_identifier if is_user_code_report_mfr_specific(device, cmd) then local reported_user_id_status = cmd.args.user_id_status - local user_code = cmd.args.user_code - local event - if reported_user_id_status == user_id_status.ENABLED_GRANT_ACCESS or -- OCCUPIED in UserCodeV1 - (reported_user_id_status == user_id_status.STATUS_NOT_AVAILABLE and user_code ~= nil) then - local code_name = LockCodesDefaults.get_code_name(device, code_id) - local change_type = LockCodesDefaults.get_change_type(device, code_id) - event = capabilities.lockCodes.codeChanged(code_id..""..change_type, { state_change = true }) - event.data = {codeName = code_name} - if code_id ~= 0 then -- ~= MASTER_CODE - LockCodesDefaults.code_set_event(device, code_id, code_name) - end - elseif code_id == 0 and reported_user_id_status == user_id_status.AVAILABLE then - local lock_codes = LockCodesDefaults.get_lock_codes(device) - for _code_id, _ in pairs(lock_codes) do - LockCodesDefaults.code_deleted(device, _code_id) - end - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(LockCodesDefaults.get_lock_codes(device)), { visibility = { displayed = false } })) - else -- user_id_status.STATUS_NOT_AVAILABLE - event = capabilities.lockCodes.codeChanged(code_id.." failed", { state_change = true }) + if credential_index == 0 and reported_user_id_status == user_id_status.AVAILABLE then + -- master code changed, clear all credentials + tables.delete_all_entries(device, "credentials") + tables.delete_all_entries(device, "users") end + else + zwave_handlers.user_code_report(self, device, cmd) + end +end - if event ~= nil then - device:emit_event(event) - end - LockCodesDefaults.clear_code_state(device, code_id) - LockCodesDefaults.verify_set_code_completion(device, cmd, code_id) +local function add_credential_handler(self, device, cmd) + local DEFAULT_COMMANDS_DELAY = 4.2 + local current_code_length = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) + local base_handler = function() + cap_handlers.add_credential(self, device, cmd) + end + if current_code_length == nil then + device:send(Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number})) + device.thread:call_with_delay(DEFAULT_COMMANDS_DELAY, base_handler) else - LockCodesDefaults.zwave_handlers[cc.USER_CODE][UserCode.REPORT](self, device, cmd) + base_handler() end end local schlage_lock = { - capability_handlers = { - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.setCodeLength.NAME] = set_code_length, - [capabilities.lockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, - [capabilities.lockCodes.commands.setCode.NAME] = set_code - } - }, zwave_handlers = { [cc.USER_CODE] = { [UserCode.REPORT] = user_code_report_handler @@ -168,6 +98,11 @@ local schlage_lock = { [Basic.SET] = basic_set_handler } }, + capability_handlers = { + [capabilities.lockCredentials.ID] = { + [capabilities.lockCredentials.commands.addCredential.NAME] = add_credential_handler + } + }, lifecycle_handlers = { doConfigure = do_configure, }, diff --git a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua index 627fe99514..af5dc4d41f 100644 --- a/drivers/SmartThings/zwave-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/zwave-lock/src/sub_drivers.lua @@ -7,5 +7,6 @@ local sub_drivers = { lazy_load_if_possible("schlage-lock"), lazy_load_if_possible("samsung-lock"), lazy_load_if_possible("keywe-lock"), + lazy_load_if_possible("legacy-handlers"), } return sub_drivers diff --git a/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua b/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua new file mode 100644 index 0000000000..ac127f234a --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua @@ -0,0 +1,198 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Integration tests for the lifecycle handlers defined in init.lua: +-- added (device_added), infoChanged (info_changed), init (LockLifecycle.init) +-- +-- Removed: doConfigure tests — z-wave drivers have no doConfigure lifecycle event. +-- Removed: init SLGA_MIGRATED without lockCodes test — z-wave init sends no protocol +-- messages; there is no NumberOfPINUsersSupported read on init. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local constants = require "lock_utils.constants" + +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +--- @type st.zwave.CommandClass.Battery +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION }, + } + } +} + +-- base-lock profile: lock + lockCodes + lockCredentials + lockUsers + battery +local mock_device_base = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, +}) + +-- Same profile but provisioning_state = "TYPED" (freshly fingerprinted) +local mock_device_typed = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, + _provisioning_state = "TYPED", +}) + +-- lock-battery profile: lock + battery only (no lockCodes / lockCredentials) +local mock_device_battery = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("lock-battery.yml"), + zwave_endpoints = zwave_lock_endpoints, +}) + +local function make_test_init(device) + return function() + test.disable_startup_messages() + test.mock_device.add_test_device(device) + end +end + +-- ============================================================================ +-- added (device_added) +-- ============================================================================ + +test.register_coroutine_test( + "added: TYPED device with lockCodes emits migrated event, persists SLGA_MIGRATED, and injects refresh", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "added" }) + + -- Migrated event emitted for TYPED+lockCodes device + test.socket.capability:__expect_send( + mock_device_typed:generate_test_message("main", + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + ) + -- inject_capability_command calls the refresh handler: + -- DoorLock:OperationGet, Battery:Get, UserCode:UsersNumberGet (no cached values) + test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device_typed.id)) + test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device_typed.id)) + test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device_typed.id)) + test.wait_for_events() + + assert( + mock_device_typed:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true, + "SLGA_MIGRATED must be true after added fires for a TYPED device" + ) + end, + { test_init = make_test_init(mock_device_typed) } +) + +test.register_coroutine_test( + "added: non-TYPED (PROVISIONED) device with lockCodes does NOT emit migrated but still injects refresh", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_base.id, "added" }) + + -- No migrated capability event expected + test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device_base.id)) + test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device_base.id)) + test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device_base.id)) + test.wait_for_events() + + assert( + mock_device_base:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) ~= true, + "SLGA_MIGRATED must NOT be set for a non-TYPED device" + ) + end, + { test_init = make_test_init(mock_device_base) } +) + +test.register_coroutine_test( + "added: device without lockCodes does NOT emit migrated event but still injects refresh", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) + + -- No migrated event; lock-battery has no lockCredentials so no UsersNumberGet + test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device_battery.id)) + test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device_battery.id)) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_battery) } +) + +-- ============================================================================ +-- init (LockLifecycle.init) +-- ============================================================================ + +test.register_coroutine_test( + "init: device with lockCodes and SLGA_MIGRATED=true emits migrated + supportedCredentials", + function() + mock_device_base:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_base.id, "init" }) + test.socket.capability:__expect_send( + mock_device_base:generate_test_message("main", + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device_base:generate_test_message("main", + capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) + ) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_base) } +) + +test.register_coroutine_test( + "init: device with lockCodes but SLGA_MIGRATED not set does nothing", + function() + -- SLGA_MIGRATED is not set; no events or z-wave sends expected + test.socket.device_lifecycle:__queue_receive({ mock_device_base.id, "init" }) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_base) } +) + +test.register_coroutine_test( + "init: lock-battery device (no lockCodes) does nothing regardless of SLGA_MIGRATED", + function() + mock_device_battery:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + -- lock-battery has no lockCodes → if branch is false → no events, no z-wave sends + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_battery) } +) + +-- ============================================================================ +-- driverSwitched (LockLifecycle.driver_switched) +-- ============================================================================ + +test.register_coroutine_test( + "driver_switched: device with lockCodes and migrated=true persists SLGA_MIGRATED and updates metadata", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "added" }) + + -- Migrated event emitted for TYPED+lockCodes device + test.socket.capability:__expect_send( + mock_device_typed:generate_test_message("main", + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + ) + -- inject_capability_command calls the refresh handler: + -- DoorLock:OperationGet, Battery:Get, UserCode:UsersNumberGet (no cached values) + test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device_typed.id)) + test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device_typed.id)) + test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device_typed.id)) + test.wait_for_events() + + -- driverSwitched occurs after added, so migrated=true is already set in the capability state cache + test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "driver_switched" }) + mock_device_typed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + assert(mock_device_typed:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true) + end, + { test_init = make_test_init(mock_device_typed) } +) + +-- ============================================================================ +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index 09d59f4861..f6554141b0 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -1,13 +1,17 @@ --- Copyright 2022 SmartThings, Inc. +-- 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 DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local lock_utils = require "lock_utils.constants".DRIVER_STATE + +test.disable_startup_messages() local KEYWE_MANUFACTURER_ID = 0x037B local KEYWE_PRODUCT_TYPE = 0x0002 @@ -24,78 +28,70 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { - profile = t_utils.get_profile_definition("base-lock.yml"), + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + _provisioning_state = "TYPED", zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, zwave_product_type = KEYWE_PRODUCT_TYPE, - zwave_product_id = KEYWE_PRODUCT_ID + zwave_product_id = KEYWE_PRODUCT_ID, } ) local function test_init() test.mock_device.add_test_device(mock_device) end + test.set_test_init_function(test_init) +local function added() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") +end + test.register_coroutine_test( "Door Lock Operation Reports unlocked should be handled", function() + added() test.socket.zwave:__queue_receive({mock_device.id, DoorLock:OperationReport({door_lock_mode = 0x00}) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( "Door Lock Operation Reports locked should be handled", function() + added() test.socket.zwave:__queue_receive({mock_device.id, DoorLock:OperationReport({door_lock_mode = 0xFF}) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) - end, - { - min_api_version = 17 - } + end ) -test.register_message_test( +test.register_coroutine_test( "Lock notification reporting should be handled", - { - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Notification:Report({notification_type = 6, event = 24}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}})) - }, - { - channel = "zwave", - direction = "receive", - message = { - mock_device.id, - Notification:Report({notification_type = 6, event = 25}) - } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}})) - } - }, - { - min_api_version = 17 - } + function() + added() + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 24}) } ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}}))) + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 25}) } ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}}))) + end ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua new file mode 100644 index 0000000000..a5c7aee7d4 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua @@ -0,0 +1,101 @@ +-- Copyright 2022 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 DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) + +local KEYWE_MANUFACTURER_ID = 0x037B +local KEYWE_PRODUCT_TYPE = 0x0002 +local KEYWE_PRODUCT_ID = 0x0001 + +-- supported comand classes +local zwave_lock_endpoints = { + { + command_classes = { + {value = DoorLock} + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, + zwave_manufacturer_id = KEYWE_MANUFACTURER_ID, + zwave_product_type = KEYWE_PRODUCT_TYPE, + zwave_product_id = KEYWE_PRODUCT_ID + } +) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Door Lock Operation Reports unlocked should be handled", + function() + test.socket.zwave:__queue_receive({mock_device.id, + DoorLock:OperationReport({door_lock_mode = 0x00}) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Door Lock Operation Reports locked should be handled", + function() + test.socket.zwave:__queue_receive({mock_device.id, + DoorLock:OperationReport({door_lock_mode = 0xFF}) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) + end, + { + min_api_version = 17 + } +) + +test.register_message_test( + "Lock notification reporting should be handled", + { + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Notification:Report({notification_type = 6, event = 24}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({data={method="manual"}})) + }, + { + channel = "zwave", + direction = "receive", + message = { + mock_device.id, + Notification:Report({notification_type = 6, event = 25}) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}})) + } + }, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_code_slga_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_code_slga_migration.lua new file mode 100644 index 0000000000..21729e507d --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_code_slga_migration.lua @@ -0,0 +1,127 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Mock out globals +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) +local t_utils = require "integration_test.utils" +--- @type st.zwave.constants +local constants = require "st.zwave.constants" +local lock_utils = require "lock_utils.constants".DRIVER_STATE + +local SCHLAGE_MANUFACTURER_ID = 0x003B +local SCHLAGE_PRODUCT_TYPE = 0x0002 +local SCHLAGE_PRODUCT_ID = 0x0469 + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION } + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints + } +) + +local schlage_mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, + zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, + zwave_product_type = SCHLAGE_PRODUCT_TYPE, + zwave_product_id = SCHLAGE_PRODUCT_ID + } +) + +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local function init_code_slot(slot_number, name, device) + local lock_codes = device.persistent_store[constants.LOCK_CODES] + if lock_codes == nil then + lock_codes = {} + device.persistent_store[constants.LOCK_CODES] = lock_codes + end + lock_codes[tostring(slot_number)] = name +end + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(schlage_mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Device called 'migrate' command", + function() + init_code_slot(1, "Zach", mock_device) + init_code_slot(5, "Steven", mock_device) + -- setup codes + test.socket.zwave:__queue_receive({mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.wait_for_events() + -- Validate migrate command + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") + end +) + +test.register_coroutine_test( + "Migrate new device", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { visibility = { displayed = false } }))) + test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device.id)) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") + end +) + +test.register_coroutine_test( + "Schlage-Lock device called 'migrate' command, validate codeLength is being properly set", + function() + init_code_slot(1, "Zach", schlage_mock_device) + init_code_slot(5, "Steven", schlage_mock_device) + -- setup codes + test.socket.zwave:__queue_receive({schlage_mock_device.id, UserCode:UsersNumberReport({ supported_users = 4 }) }) + test.socket.zwave:__queue_receive({schlage_mock_device.id, Configuration:Report({ parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, configuration_value = 6 })}) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6))) + test.wait_for_events() + -- Validate migrate command + test.socket.capability:__queue_receive({ schlage_mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1}, {credentialIndex=5, credentialType="pin", userIndex=2}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=2, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( schlage_mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.wait_for_events() + assert(schlage_mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua new file mode 100644 index 0000000000..96b192a03d --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua @@ -0,0 +1,480 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Integration tests for the lockCredentials capability commands: +-- addCredential, updateCredential, deleteCredential, deleteAllCredentials + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local json = require "st.json" +local zw = require "st.zwave" +local table_utils = require "lock_utils.tables" +local constants = require "lock_utils.constants" +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local access_control_event = Notification.event.access_control + +test.disable_startup_messages() + + +if table_utils.find_all_entries_by == nil then + function table_utils.find_all_entries_by(device, table_name, key, value) + local entries = table_utils.get_state(device, table_name) or {} + local matches = {} + for _, entry in ipairs(entries) do + if entry[key] == value then table.insert(matches, entry) end + end + return matches + end +end + + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, +}) +local mock_latest_state = {} +local function mock_state_key(component_id, capability_id, attribute_name) + return table.concat({ component_id, capability_id, attribute_name }, "|") +end + +local function install_state_mocks() + mock_latest_state = {} + mock_device.get_latest_state = function(_, component_id, capability_id, attribute_name, default_value) + local value = mock_latest_state[mock_state_key(component_id, capability_id, attribute_name)] + if value == nil then + if capability_id == capabilities.lockCredentials.ID and attribute_name == capabilities.lockCredentials.credentials.NAME then + value = mock_device.persistent_store.persistedCredentials + elseif capability_id == capabilities.lockUsers.ID and attribute_name == capabilities.lockUsers.users.NAME then + value = mock_device.persistent_store.persistedUsers + end + end + if value == nil then return default_value end + return value + end + local original_set_field = mock_device.set_field + mock_device.set_field = function(_, key, value, opts) + if opts and opts.persist then + mock_device.persistent_store[key] = value + else + mock_device.transient_store[key] = value + end + if original_set_field then original_set_field(mock_device, key, value, opts) end + end + mock_device.emit_event = function(_, event) + mock_latest_state[mock_state_key("main", event.capability.ID, event.attribute.NAME)] = event.value.value + local message = mock_device:generate_test_message("main", event) + test.socket.capability:send(message[1], json.encode(message[2])) + end +end + +-- ── helpers ──────────────────────────────────────────────────────────────── + +local function test_init() + mock_device.persistent_store = mock_device.persistent_store or {} + mock_device.transient_store = mock_device.transient_store or {} + install_state_mocks() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local function expect_set_pin_code(user_identifier, user_code) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = user_identifier, + user_code = user_code, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS, + }):build_test_tx(mock_device.id) + ) +end + +local function queue_user_code_added(user_identifier) + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.NEW_USER_CODE_ADDED, + event_parameter = string.char(user_identifier), + }) }) +end + +local function seed_users(entries) + for _, entry in ipairs(entries) do + local so_far = {} + for _, e in ipairs(entries) do + table.insert(so_far, e) + if e == entry then break end + end + local event = capabilities.lockUsers.users(so_far, { visibility = { displayed = false } }) + mock_latest_state[mock_state_key("main", event.capability.ID, event.attribute.NAME)] = event.value.value + end + mock_device.persistent_store.persistedUsers = entries + test.wait_for_events() +end + +local function seed_credentials(entries) + for _, entry in ipairs(entries) do + local so_far = {} + for _, e in ipairs(entries) do + table.insert(so_far, e) + if e == entry then break end + end + local event = capabilities.lockCredentials.credentials(so_far, { visibility = { displayed = false } }) + mock_latest_state[mock_state_key("main", event.capability.ID, event.attribute.NAME)] = event.value.value + end + mock_device.persistent_store.persistedCredentials = entries + test.wait_for_events() +end + +-- ============================================================================ +-- addCredential +-- ============================================================================ + +test.register_coroutine_test( + "addCredential: sends SetPINCode to the lock and emits success when the lock acknowledges", + function() + -- Queue the capability command: userIndex=1, userType="guest", credentialType="pin", credentialData="1234" + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 1, "guest", "pin", "1234" } }, + }) + + -- Expect SetPINCode to be sent to the lock + expect_set_pin_code(1, "1234") + test.wait_for_events() + + -- Lock responds with SUCCESS + queue_user_code_added(1) + + -- Handler adds the credential to the credentials table. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }, + { visibility = { displayed = false } } + )) + ) + -- commandResult with userIndex and credentialIndex + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential: emits duplicate when the lock returns DUPLICATE_CODE", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 1, "guest", "pin", "1234" } }, + }) + + expect_set_pin_code(1, "1234") + test.wait_for_events() + + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, event = access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE, event_parameter = string.char(1) }) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "duplicate" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential: returns busy when another operation is already in progress", + function() + -- Put the device into busy state by starting an addCredential operation + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 1, "guest", "pin", "1234" } }, + }) + expect_set_pin_code(1, "1234") + test.wait_for_events() + + -- Second addCredential while first is still pending → should get busy + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 2, "guest", "pin", "5678" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- updateCredential +-- ============================================================================ + +test.register_coroutine_test( + "updateCredential: returns failure when cached credential state is unavailable", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 1, 1, "pin", "newPin9" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateCredential: returns failure immediately when the credential does not exist in the table", + function() + -- No credentials seeded — credentialIndex 99 does not exist + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 99, 99, "pin", "badPin" } }, + }) + + -- No z-wave message should be sent + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateCredential: returns busy when another operation is already in progress", + function() + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.ADD, {}) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 1, 1, "pin", "pin2222" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteCredential +-- ============================================================================ + +test.register_coroutine_test( + "deleteCredential: returns failure when credential state is unavailable", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteCredential: returns failure when credential state is unavailable", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteCredential: returns failure immediately when the credentialIndex does not exist in the table", + function() + -- No credentials seeded — index 5 is unknown + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 5, "pin" } }, + }) + + -- No z-wave messages should be sent + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteCredential: returns busy when another operation is already in progress", + function() + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.ADD, {}) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteAllCredentials +-- ============================================================================ + +test.register_coroutine_test( + "deleteAllCredentials: sends ClearAllPINCodes and emits success when the lock returns PASS", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + }) + seed_credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + + test.timer.__create_and_queue_test_time_advance_timer(4, "oneshot") + test.wait_for_events() + + test.mock_time.advance_time(4) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllCredentials: emits busy when another operation is already in progress", + function() + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.ADD, {}) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllCredentials: returns busy when another operation is already in progress", + function() + seed_credentials({ { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }) + + -- First deleteAllCredentials starts the z-wave flow (device is now busy) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + -- deleteAllCredentials sends individual UserCode:Set AVAILABLE commands + test.wait_for_events() + + -- Second deleteAllCredentials while first is pending → busy + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_pre_configured.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_pre_configured.lua new file mode 100644 index 0000000000..0895ff3215 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_pre_configured.lua @@ -0,0 +1,514 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Integration tests for lockUsers and lockCredentials commands with state +-- pre-configured before each test. Two users and two credentials are seeded +-- at the start of every test so tests can focus on the various response states +-- produced by the z-wave response handlers in commands.lua. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local table_utils = require "lock_utils.tables" +local constants = require "lock_utils.constants" +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local access_control_event = Notification.event.access_control +local json = require "st.json" + + +test.disable_startup_messages() +if table_utils.find_all_entries_by == nil then + function table_utils.find_all_entries_by(device, table_name, key, value) + local entries = table_utils.get_state(device, table_name) or {} + local matches = {} + for _, entry in ipairs(entries) do + if entry[key] == value then table.insert(matches, entry) end + end + return matches + end +end + + +-- ── Shared device ────────────────────────────────────────────────────────── + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, +}) + +-- Lightweight mock state so that table_utils functions work on mock_device +-- before the driver has lazily initialised the wrapped device. After the +-- driver processes its first message (wrapped_init), MockDevice.__index +-- delegates every field access to the real driver device, so the overrides +-- below are only ever active during the pre-init seeding phase. +local mock_latest_state = {} +local function mock_state_key(component_id, capability_id, attribute_name) + return table.concat({ component_id, capability_id, attribute_name }, "|") +end + +local function install_state_mocks() + mock_latest_state = {} + + -- tables.lua calls device.log.{debug,warn,error} unconditionally. + rawset(mock_device, "log", { + debug = function() end, + info = function() end, + warn = function() end, + error = function() end, + }) + + -- get_state guards with device:supports_capability; always return true here. + rawset(mock_device, "supports_capability", function() return true end) + + -- get_state / get_max_entries use device:get_latest_state to read + -- capability attribute values from the state cache. + rawset(mock_device, "get_latest_state", + function(_, component_id, capability_id, attribute_name, default_value) + local key = mock_state_key(component_id, capability_id, attribute_name) + local value = mock_latest_state[key] + if value == nil then return default_value end + return value + end + ) + + -- add_entry / delete_entry / update_entry all call device:emit_event. + -- Forward to the capability socket so __expect_send checks pass, and + -- keep mock_latest_state in sync so that successive get_state calls + -- within the same seeding loop see the growing list. + rawset(mock_device, "emit_event", function(_, event) + mock_latest_state[mock_state_key("main", event.capability.ID, event.attribute.NAME)] = event.value.value + local message = mock_device:generate_test_message("main", event) + test.socket.capability:send(message[1], json.encode(message[2])) + end) +end + +local function test_init() + install_state_mocks() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local function expect_set_pin_code(user_identifier, user_code) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = user_identifier, + user_code = user_code, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS, + }):build_test_tx(mock_device.id) + ) +end + +local function expect_clear_pin_code(user_identifier) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = user_identifier, + user_id_status = UserCode.user_id_status.AVAILABLE, + }):build_test_tx(mock_device.id) + ) +end + +local function queue_user_code_added(user_identifier) + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.NEW_USER_CODE_ADDED, + event_parameter = string.char(user_identifier), + }) }) +end + +local function queue_user_code_deleted(user_identifier) + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = access_control_event.SINGLE_USER_CODE_DELETED, + event_parameter = string.char(user_identifier), + }) }) +end + +-- ── Seeding helpers ──────────────────────────────────────────────────────── + +local INITIAL_USERS = { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, +} +local INITIAL_CREDS = { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, +} + +-- Seed a list of entries into a named table, consuming the resulting events. +local function seed_table(attribute_fn, table_name, entries) + local accumulated = {} + for _, entry in ipairs(entries) do + table.insert(accumulated, entry) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + attribute_fn(accumulated, { visibility = { displayed = false } })) + ) + assert( + table_utils.add_entry(mock_device, table_name, entry) == constants.COMMAND_RESULT.SUCCESS, + "seed_table: add_entry failed for " .. table_name + ) + end + test.wait_for_events() +end + +-- Pre-configure each test with 2 users and 2 credentials. +local function setup_state() + seed_table(capabilities.lockUsers.users, "users", INITIAL_USERS) + seed_table(capabilities.lockCredentials.credentials, "credentials", INITIAL_CREDS) +end + +-- ============================================================================ +-- addUser — pre-configured device (2 users already present) +-- ============================================================================ + +test.register_coroutine_test( + "addUser (pre-configured): assigns the next available index (3) when two users already exist", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Carol", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 3 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- updateUser — pre-configured device +-- ============================================================================ + +test.register_coroutine_test( + "updateUser (pre-configured): updates Alice's name and emits success with userIndex", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 1, "AliceRenamed", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "AliceRenamed", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateUser (pre-configured): returns failure for a userIndex that does not exist", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 10, "Ghost", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 10, userName = "Ghost", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 10 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteUser with associated credential — exercises clear_pin_code_response +-- ============================================================================ + +test.register_coroutine_test( + "deleteUser (pre-configured, PASS): removes both user and credential and emits success for each", + function() + setup_state() + + -- Delete user 1 who has credential 1 → driver injects deleteCredential → ClearPINCode + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 1 } }, + }) + expect_clear_pin_code(1) + test.wait_for_events() + + queue_user_code_deleted(1) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 2, userName = "Bob", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 2, credentialIndex = 2, credentialType = "pin" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- addCredential response states — set_pin_code_response +-- ============================================================================ + +test.register_coroutine_test( + "addCredential (pre-configured): succeeds for a new slot and emits userIndex + credentialIndex", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 3, "guest", "pin", "pin03", "Guest 3" } }, + }) + + expect_set_pin_code(3, "pin03") + test.wait_for_events() + + queue_user_code_added(1) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin", credentialName = "Guest 3" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", userIndex = 3, credentialIndex = 3 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential (pre-configured): emits duplicate when the lock rejects with DUPLICATE_CODE", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 3, "guest", "pin", "pin01" } }, + }) + + expect_set_pin_code(3, "pin01") + test.wait_for_events() + + test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, event = access_control_event.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE, event_parameter = string.char(1) }) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "duplicate" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential (pre-configured): emits failure when the lock returns available", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 3, "guest", "pin", "pin03" } }, + }) + + expect_set_pin_code(3, "pin03") + test.wait_for_events() + + test.socket.zwave:__queue_receive({ mock_device.id, UserCode:Report({ user_identifier = 1, user_id_status = UserCode.user_id_status.AVAILABLE }) }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- updateCredential response states — set_pin_code_response +-- ============================================================================ + +test.register_coroutine_test( + "updateCredential (pre-configured): succeeds and emits success with userIndex and credentialIndex", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 1, 1, "pin", "newPin1" } }, + }) + + expect_set_pin_code(1, "newPin1") + test.wait_for_events() + + queue_user_code_added(1) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateCredential (pre-configured): returns failure immediately for a non-existent credentialIndex", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 99, 99, "pin", "badPin" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteCredential standalone (lockCredentials.DELETE path) +-- ============================================================================ + +test.register_coroutine_test( + "deleteCredential (pre-configured, PASS): removes credential and emits success with indices", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + expect_clear_pin_code(1) + test.wait_for_events() + + queue_user_code_deleted(1) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 2, credentialIndex = 2, credentialType = "pin" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_tables.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_tables.lua new file mode 100644 index 0000000000..63f846cf49 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_tables.lua @@ -0,0 +1,761 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Unit tests for lock_utils/tables.lua +-- Tests directly call table_utils functions and verify both return values +-- and emitted capability events. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local json = require "st.json" +local capabilities = require "st.capabilities" +local st_utils = require "st.utils" +local table_utils = require "lock_utils.tables" +local constants = require "lock_utils.constants" + +test.disable_startup_messages() + +-- --------------------------------------------------------------------------- +-- Shared mock device +-- --------------------------------------------------------------------------- + +local zw = require "st.zwave" + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, +}) + +local mock_latest_state = {} +local mock_persistent_fields = {} +local mock_transient_fields = {} + +local function mock_state_key(component_id, capability_id, attribute_name) + return table.concat({ component_id, capability_id, attribute_name }, "|") +end + +local function install_table_utils_device_mocks() + mock_latest_state = {} + mock_persistent_fields = {} + mock_transient_fields = {} + mock_device.supports_capability = function() return true end + mock_device.log = { error = function() end, warn = function() end } + mock_device.get_latest_state = function(_, component_id, capability_id, attribute_name, default_value) + local value = mock_latest_state[mock_state_key(component_id, capability_id, attribute_name)] + if value == nil then return default_value end + return value + end + mock_device.emit_event = function(_, event) + mock_latest_state[mock_state_key("main", event.capability.ID, event.attribute.NAME)] = event.value.value + local message = mock_device:generate_test_message("main", event) + test.socket.capability:send(message[1], json.encode(message[2])) + end + mock_device.set_field = function(_, key, value, opts) + if opts and opts.persist then + mock_persistent_fields[key] = value + else + mock_transient_fields[key] = value + end + end + mock_device.get_field = function(_, key) + if mock_transient_fields[key] ~= nil then return mock_transient_fields[key] end + return mock_persistent_fields[key] + end + -- tables.lua calls device.log.{debug,warn,error} unconditionally. + rawset(mock_device, "log", { + debug = function() end, + info = function() end, + warn = function() end, + error = function() end, + }) +end + + +local function test_init() + test.mock_device.add_test_device(mock_device) + install_table_utils_device_mocks() +end +test.set_test_init_function(test_init) + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +-- Seed the users table with `entries` and consume the resulting emit_events. +-- After this call the state cache has those entries and the socket is clean. +local function seed_users(entries) + for _, entry in ipairs(entries) do + -- Build the expected post-insert table up to this entry. + local expected = {} + for _, e in ipairs(entries) do + table.insert(expected, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(expected, { visibility = { displayed = false } })) + ) + local result = table_utils.add_entry(mock_device, "users", entry) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "seed_users: add_entry failed for entry userIndex=" .. tostring(entry.userIndex)) + end +end + +-- Seed the credentials table with `entries` and consume the resulting emit_events. +local function seed_credentials(entries) + for _, entry in ipairs(entries) do + local expected = {} + for _, e in ipairs(entries) do + table.insert(expected, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials(expected, { visibility = { displayed = false } })) + ) + local result = table_utils.add_entry(mock_device, "credentials", entry) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "seed_credentials: add_entry failed for entry credentialIndex=" .. tostring(entry.credentialIndex)) + end +end + +-- =========================================================================== +-- add_entry +-- =========================================================================== + +test.register_coroutine_test( + "add_entry: adds a new user entry and emits the updated table", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({ entry }, { visibility = { displayed = false } })) + ) + + local result = table_utils.add_entry(mock_device, "users", entry) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: returns OCCUPIED when an entry with the same userIndex already exists", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ entry }) + + -- Attempt to add a different entry with the same userIndex (match_key) + local duplicate = { userIndex = 1, userType = "unrestricted", userName = "Bob" } + local result = table_utils.add_entry(mock_device, "users", duplicate) + assert(result == constants.COMMAND_RESULT.OCCUPIED, + "Expected OCCUPIED, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: returns RESOURCE_EXHAUSTED when table is at max capacity", + function() + local entries = {} + for i = 1, 20 do + entries[i] = { userIndex = i, userType = "guest", userName = "User" .. i } + end + seed_users(entries) + + local overflow = { userIndex = 21, userType = "guest", userName = "Overflow" } + local result = table_utils.add_entry(mock_device, "users", overflow) + assert(result == constants.COMMAND_RESULT.RESOURCE_EXHAUSTED, + "Expected RESOURCE_EXHAUSTED, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: returns FAILURE when a required key is missing", + function() + -- userType is required for users table + local incomplete = { userIndex = 1 } + local result = table_utils.add_entry(mock_device, "users", incomplete) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: returns FAILURE for an unknown table name", + function() + local result = table_utils.add_entry(mock_device, "nonexistent_table", + { userIndex = 1, userType = "guest" }) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: adds a credential entry and emits updated credentials table", + function() + local entry = { userIndex = 1, credentialIndex = 1, credentialType = "pin" } + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({ entry }, { visibility = { displayed = false } })) + ) + + local result = table_utils.add_entry(mock_device, "credentials", entry) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- update_entry +-- =========================================================================== + +test.register_coroutine_test( + "update_entry: updates an existing user entry and emits updated table", + function() + local original = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ original }) + + local expected_after_update = { + { userIndex = 1, userType = "adminMember", userName = "Alice_Updated" } + } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(expected_after_update, { visibility = { displayed = false } })) + ) + + local result = table_utils.update_entry(mock_device, "users", 1, + { userName = "Alice_Updated", userType = "adminMember" }) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "update_entry: returns FAILURE when no entry matches the match_key value", + function() + local result = table_utils.update_entry(mock_device, "users", 99, + { userName = "Ghost" }) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "update_entry: only updates the specified fields, leaves others intact", + function() + local entry1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local entry2 = { userIndex = 2, userType = "guest", userName = "Bob" } + seed_users({ entry1, entry2 }) + + -- Update only userName of entry 2; userType should remain "guest" + local expected = { + { userIndex = 1, userType = "guest", userName = "Alice" }, + { userIndex = 2, userType = "guest", userName = "Bob_Renamed" }, + } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(expected, { visibility = { displayed = false } })) + ) + + local result = table_utils.update_entry(mock_device, "users", 2, { userName = "Bob_Renamed" }) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "update_entry: returns FAILURE for an unknown table name", + function() + local result = table_utils.update_entry(mock_device, "bad_table", 1, { userName = "X" }) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- delete_entry +-- =========================================================================== + +test.register_coroutine_test( + "delete_entry: deletes an existing entry and returns COMMAND_RESULT.SUCCESS", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ entry }) + + -- After deletion the table is empty + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_entry(mock_device, "users", 1) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "delete_entry: returns FAILURE when no entry matches the match_key value", + function() + local result = table_utils.delete_entry(mock_device, "users", 99) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "delete_entry: remaining entries are intact after a deletion", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + local e3 = { userIndex = 3, userType = "guest", userName = "Carol" } + seed_users({ e1, e2, e3 }) + + -- Delete the middle entry; expect e1 and e3 remain + local expected_remaining = { + { userIndex = 1, userType = "guest", userName = "Alice" }, + { userIndex = 3, userType = "guest", userName = "Carol" }, + } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(expected_remaining, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_entry(mock_device, "users", 2) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "delete_entry: returns FAILURE for an unknown table name", + function() + local result = table_utils.delete_entry(mock_device, "bad_table", 1) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- delete_all_entries +-- =========================================================================== + +test.register_coroutine_test( + "delete_all_entries: emits an empty users table and returns SUCCESS", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + seed_users({ e1, e2 }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_all_entries(mock_device, "users") + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "delete_all_entries: returns SUCCESS even when the table is already empty", + function() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_all_entries(mock_device, "users") + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "delete_all_entries: returns FAILURE for an unknown table name", + function() + local result = table_utils.delete_all_entries(mock_device, "bad_table") + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "delete_all_entries: emits an empty credentials table and returns SUCCESS", + function() + local cred = { userIndex = 1, credentialIndex = 1, credentialType = "pin" } + seed_credentials({ cred }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_all_entries(mock_device, "credentials") + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- find_entry +-- =========================================================================== + +test.register_coroutine_test( + "find_entry: returns the matching entry when it exists", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + seed_users({ e1, e2 }) + + local result = table_utils.find_entry(mock_device, "users", 2) + assert(type(result) == "table", + "Expected a table entry, got: " .. tostring(result)) + assert(result.userIndex == 2, + "Expected userIndex == 2, got: " .. tostring(result.userIndex)) + assert(result.userName == "Bob", + "Expected userName == 'Bob', got: " .. tostring(result.userName)) + end +) + +test.register_coroutine_test( + "find_entry: returns nil when no entry matches the value", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ e1 }) + + local result = table_utils.find_entry(mock_device, "users", 99) + assert(result == nil, + "Expected nil for missing entry, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "find_entry: finds a credential entry by credentialIndex", + function() + local cred = { userIndex = 1, credentialIndex = 5, credentialType = "pin" } + seed_credentials({ cred }) + + local result = table_utils.find_entry(mock_device, "credentials", 5) + assert(type(result) == "table", + "Expected a table entry, got: " .. tostring(result)) + assert(result.credentialIndex == 5, + "Expected credentialIndex == 5, got: " .. tostring(result.credentialIndex)) + end +) + +-- =========================================================================== +-- find_entry_by +-- =========================================================================== + +test.register_coroutine_test( + "find_entry_by: returns the entry matching an arbitrary key", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "adminMember", userName = "Bob" } + seed_users({ e1, e2 }) + + local result = table_utils.find_entry_by(mock_device, "users", "userName", "Bob") + assert(type(result) == "table", + "Expected a table entry, got: " .. tostring(result)) + assert(result.userIndex == 2, + "Expected userIndex == 2, got: " .. tostring(result.userIndex)) + end +) + +test.register_coroutine_test( + "find_entry_by: returns nil when no entry matches", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ e1 }) + + local result = table_utils.find_entry_by(mock_device, "users", "userName", "Nobody") + assert(result == nil, + "Expected nil for unmatched key/value, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "find_entry_by: returns the first matching entry when multiple entries share the same value", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + local e3 = { userIndex = 3, userType = "guest", userName = "Carol" } + seed_users({ e1, e2, e3 }) + + local result = table_utils.find_entry_by(mock_device, "users", "userType", "guest") + assert(type(result) == "table", + "Expected a table entry, got: " .. tostring(result)) + assert(result.userIndex == 1, + "Expected first match userIndex == 1, got: " .. tostring(result.userIndex)) + end +) + +-- =========================================================================== +-- next_index +-- =========================================================================== + +test.register_coroutine_test( + "next_index: returns 1 when the table is empty", + function() + local result = table_utils.next_index(mock_device, "users") + assert(result == 1, + "Expected 1 for empty table, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "next_index: returns the next sequential index after a contiguous range", + function() + local entries = {} + for i = 1, 3 do + entries[i] = { userIndex = i, userType = "guest", userName = "User" .. i } + end + seed_users(entries) + + local result = table_utils.next_index(mock_device, "users") + assert(result == 4, + "Expected 4 after indices 1-3, got: " .. tostring(result)) + end +) + +-- this should not happen during normal operation. +test.register_coroutine_test( + "next_index: returns the lowest gap when indices are non-contiguous", + function() + -- Insert entries at indices 1 and 3, leaving a gap at 2 + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e3 = { userIndex = 3, userType = "guest", userName = "Carol" } + seed_users({ e1, e3 }) + + local result = table_utils.next_index(mock_device, "users") + assert(result == 2, + "Expected 2 as the lowest gap, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- get_max_entries +-- =========================================================================== + +test.register_coroutine_test( + "get_max_entries: returns the default of 20 when the attribute is not set", + function() + local result = table_utils.get_max_entries(mock_device, "users") + assert(result == 20, + "Expected default max of 20, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "get_max_entries: returns the default of 20 for the credentials table when the attribute is not set", + function() + local result = table_utils.get_max_entries(mock_device, "credentials") + assert(result == 20, + "Expected default max of 20, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- Persistence — mutations write to the persistent store +-- =========================================================================== + +test.register_coroutine_test( + "persist: add_entry writes the new users table to the persistent store immediately", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({ entry }, { visibility = { displayed = false } })) + ) + table_utils.add_entry(mock_device, "users", entry) + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedUsers")) + assert(type(persisted) == "table" and #persisted == 1, + "Expected 1 persisted user, got: " .. tostring(persisted and #persisted)) + assert(persisted[1].userIndex == 1 and persisted[1].userName == "Alice", + "Persisted user data does not match the added entry") + end +) + +test.register_coroutine_test( + "persist: update_entry writes the updated users table to the persistent store immediately", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ entry }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userType = "adminMember", userName = "Alice" } }, + { visibility = { displayed = false } })) + ) + table_utils.update_entry(mock_device, "users", 1, { userType = "adminMember" }) + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedUsers")) + assert(type(persisted) == "table" and #persisted == 1, + "Expected 1 persisted user after update") + assert(persisted[1].userType == "adminMember", + "Persisted user type was not updated, got: " .. tostring(persisted[1].userType)) + end +) + +test.register_coroutine_test( + "persist: delete_entry removes the entry from the persistent store immediately", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + seed_users({ e1, e2 }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({ e2 }, { visibility = { displayed = false } })) + ) + table_utils.delete_entry(mock_device, "users", 1) + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedUsers")) + assert(type(persisted) == "table" and #persisted == 1, + "Expected 1 persisted user after delete, got: " .. tostring(persisted and #persisted)) + assert(persisted[1].userIndex == 2, + "Expected remaining user to have userIndex == 2, got: " .. tostring(persisted[1].userIndex)) + end +) + +test.register_coroutine_test( + "persist: delete_all_entries writes an empty table to the persistent store", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ entry }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + table_utils.delete_all_entries(mock_device, "users") + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedUsers")) + assert(type(persisted) == "table" and #persisted == 0, + "Expected empty persistent store after delete_all, got: " .. tostring(persisted and #persisted)) + end +) + +test.register_coroutine_test( + "persist: add_entry writes the new credentials table to the persistent store immediately", + function() + local cred = { userIndex = 1, credentialIndex = 1, credentialType = "pin" } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({ cred }, { visibility = { displayed = false } })) + ) + table_utils.add_entry(mock_device, "credentials", cred) + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedCredentials")) + assert(type(persisted) == "table" and #persisted == 1, + "Expected 1 persisted credential, got: " .. tostring(persisted and #persisted)) + assert(persisted[1].credentialIndex == 1, + "Persisted credential index does not match") + end +) + +-- =========================================================================== +-- Persistence — get_state falls back to the persistent store +-- =========================================================================== + +test.register_coroutine_test( + "persist: get_state returns data from the persistent store when capability state cache is absent", + function() + -- The device was loaded with a pre-seeded persistent field (set in test_init + -- below), but no capability event has been emitted for users, so get_latest_state + -- returns nil. get_state must fall back to the persistent store. + local state = table_utils.get_state(mock_device, "users") + assert(type(state) == "table" and #state == 1, + "Expected 1 user from persistent-store fallback, got: " .. tostring(state and #state)) + assert(state[1].userIndex == 1 and state[1].userName == "Alice", + "Fallback data does not match pre-seeded persistent entry") + end, + { + test_init = function() + -- Pre-seed persistent store BEFORE add_test_device so that wrapped_init + -- copies the field into the device's persistent_store on startup. + mock_device:set_field( + "persistedUsers", + { { userIndex = 1, userType = "guest", userName = "Alice" } }, + { persist = true } + ) + test.mock_device.add_test_device(mock_device) + end, + } +) + +-- =========================================================================== +-- Persistence — restore_from_persistent_store re-emits stored tables +-- =========================================================================== + +test.register_coroutine_test( + "persist: restore_from_persistent_store emits users capability event for stored data", + function() + local user = { userIndex = 1, userType = "guest", userName = "Alice" } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({ user }, { visibility = { displayed = false } })) + ) + table_utils.restore_from_persistent_store(mock_device) + end, + { + test_init = function() + install_table_utils_device_mocks() + mock_device:set_field( + "persistedUsers", + { { userIndex = 1, userType = "guest", userName = "Alice" } }, + { persist = true } + ) + test.mock_device.add_test_device(mock_device) + end, + } +) + +test.register_coroutine_test( + "persist: restore_from_persistent_store emits credentials capability event for stored data", + function() + local cred = { userIndex = 1, credentialIndex = 1, credentialType = "pin" } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({ cred }, { visibility = { displayed = false } })) + ) + table_utils.restore_from_persistent_store(mock_device) + end, + { + test_init = function() + install_table_utils_device_mocks() + mock_device:set_field( + "persistedCredentials", + { { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }, + { persist = true } + ) + test.mock_device.add_test_device(mock_device) + end, + } +) + +test.register_coroutine_test( + "persist: restore_from_persistent_store is a no-op when the persistent store is empty", + function() + -- No capability events expected when there is nothing in the persistent store. + table_utils.restore_from_persistent_store(mock_device) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua new file mode 100644 index 0000000000..da39c8c692 --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua @@ -0,0 +1,705 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Integration tests for the lockUsers capability commands: +-- addUser, updateUser, deleteUser, deleteAllUsers + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local table_utils = require "lock_utils.tables" +local constants = require "lock_utils.constants" +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local json = require "st.json" + + +test.disable_startup_messages() + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, +}) + +-- ── helpers ──────────────────────────────────────────────────────────────── + +-- Lightweight mock state so that table_utils functions work on mock_device +-- before the driver has lazily initialised the wrapped device. After the +-- driver processes its first message (wrapped_init), MockDevice.__index +-- delegates every field access to the real driver device, so the overrides +-- below are only ever active during the pre-init seeding phase. +local mock_latest_state = {} +local function mock_state_key(component_id, capability_id, attribute_name) + return table.concat({ component_id, capability_id, attribute_name }, "|") +end + +local function install_state_mocks() + mock_latest_state = {} + + -- tables.lua calls device.log.{debug,warn,error} unconditionally. + rawset(mock_device, "log", { + debug = function() end, + info = function() end, + warn = function() end, + error = function() end, + }) + + -- get_state guards with device:supports_capability; always return true here. + rawset(mock_device, "supports_capability", function() return true end) + + -- get_state / get_max_entries use device:get_latest_state to read + -- capability attribute values from the state cache. + rawset(mock_device, "get_latest_state", + function(_, component_id, capability_id, attribute_name, default_value) + local key = mock_state_key(component_id, capability_id, attribute_name) + local value = mock_latest_state[key] + if value == nil then return default_value end + return value + end + ) + + -- add_entry / delete_entry / update_entry all call device:emit_event. + -- Forward to the capability socket so __expect_send checks pass, and + -- keep mock_latest_state in sync so that successive get_state calls + -- within the same seeding loop see the growing list. + rawset(mock_device, "emit_event", function(_, event) + mock_latest_state[mock_state_key("main", event.capability.ID, event.attribute.NAME)] = event.value.value + local message = mock_device:generate_test_message("main", event) + test.socket.capability:send(message[1], json.encode(message[2])) + end) +end + +local function test_init() + install_state_mocks() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local function expect_set_pin_code(user_identifier, user_code) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = user_identifier, + user_code = user_code, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS, + }):build_test_tx(mock_device.id) + ) +end + +local function expect_clear_pin_code(user_identifier) + test.socket.zwave:__expect_send( + UserCode:Set({ + user_identifier = user_identifier, + user_id_status = UserCode.user_id_status.AVAILABLE, + }):build_test_tx(mock_device.id) + ) +end + +-- Directly insert users into the device state via table_utils (mirrors test_lock_tables.lua). +-- Consumes the resulting capability events so the socket queue stays clean. +local function seed_users(entries) + for _, entry in ipairs(entries) do + local so_far = {} + for _, e in ipairs(entries) do + table.insert(so_far, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(so_far, { visibility = { displayed = false } })) + ) + assert(table_utils.add_entry(mock_device, "users", entry) == constants.COMMAND_RESULT.SUCCESS, + "seed_users: add_entry failed for userIndex=" .. tostring(entry.userIndex)) + end + test.wait_for_events() +end + +-- Directly insert credentials into the device state. +local function seed_credentials(entries) + for _, entry in ipairs(entries) do + local so_far = {} + for _, e in ipairs(entries) do + table.insert(so_far, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials(so_far, { visibility = { displayed = false } })) + ) + assert(table_utils.add_entry(mock_device, "credentials", entry) == constants.COMMAND_RESULT.SUCCESS, + "seed_credentials: add_entry failed for credentialIndex=" .. tostring(entry.credentialIndex)) + end + test.wait_for_events() +end + +-- Set totalUsersSupported (and pinUsersSupported) by injecting the Z-Wave +-- UsersNumberReport that the real users_number_report handler processes. +-- Using the Z-Wave path is essential: it goes through the driver's normal +-- emit_component_event path and populates the driver device's state cache, +-- which is what get_max_entries reads when enforcing the slot limit. +local function set_total_users_supported(n) + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:UsersNumberReport({ supported_users = n }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.totalUsersSupported(n, { state_change = true, visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.pinUsersSupported(n, { state_change = true, visibility = { displayed = false } })) + ) + test.wait_for_events() +end + +-- ============================================================================ +-- addUser +-- ============================================================================ + +test.register_coroutine_test( + "addUser: assigns userIndex 1 for the first user and emits a success commandResult", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Alice", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Alice", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addUser: assigns the next sequential userIndex when users already exist", + function() + seed_users({ { userIndex = 1, userName = "Alice", userType = "guest" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Bob", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addUser: fills a gap left by a deleted user rather than appending beyond max", + function() + -- Seed indices 1 and 3; index 2 is the expected gap to fill + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Bob", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addUser: returns resourceExhausted when totalUsersSupported has been reached", + function() + set_total_users_supported(2) + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Carol", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "resourceExhausted" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- updateUser +-- ============================================================================ + +test.register_coroutine_test( + "updateUser: updates an existing user and emits a success commandResult with userIndex", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 1, "AliceUpdated", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "AliceUpdated", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateUser: creates user when userIndex does not exist", + function() + -- empty table — nothing to update + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 99, "Ghost", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 99, userName = "Ghost", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 99 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateUser: can change a user's type as well as name", + function() + seed_users({ { userIndex = 1, userName = "Alice", userType = "guest" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 1, "Alice", "adminMember" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Alice", userType = "adminMember" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteUser (no associated credential — pure local delete) +-- ============================================================================ + +test.register_coroutine_test( + "deleteUser: removes a user with no credential and emits a success commandResult with userIndex", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 1 } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 2, userName = "Bob", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteUser: returns failure when the target userIndex is not in the users table", + function() + -- empty table + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 99 } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteAllUsers (injects deleteAllCredentials → ClearAllPINCodes z-wave flow) +-- ============================================================================ + +test.register_coroutine_test( + "deleteAllUsers: sends ClearAllPINCodes and emits success for both users and credentials on PASS", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }) + seed_credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteAllUsers", args = {} }, + }) + + test.timer.__create_and_queue_test_time_advance_timer(0, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(0.5, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(8, "oneshot") + test.mock_time.advance_time(0) + expect_clear_pin_code(1) + test.wait_for_events() + test.mock_time.advance_time(0.5) + expect_clear_pin_code(2) + test.wait_for_events() + + -- The legacy subdriver consumes deletion notifications for this mock profile; advance + -- to the driver's cleanup timer, which clears both tables and reports success. + test.mock_time.advance_time(8) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllUsers: emits busy when another lock command is in progress", + function() + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.ADD, {}) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteAllUsers", args = {} }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- State-consistency: add → delete → re-add (indices 1, 2, 3 lifecycle) +-- ============================================================================ +-- +-- These tests verify that the users and credentials tables stay in sync +-- through a full lifecycle: populate three slots, remove the middle one, +-- then re-add a user and credential into the freed slot. The goal is to +-- confirm there is no stale index state that would cause duplicate entries, +-- wrong slot assignment, or mismatched user↔credential links. + + +test.register_coroutine_test( + "State-consistency: add users 1-3, deleteUser 2 (no credential), re-add user reclaims index 2", + function() + -- Populate three user slots directly (no credentials, so deleteUser will take + -- the no-Z-Wave path and delete the user entry locally). + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }) + + -- Delete user at index 2; no credential is linked so this is a pure local delete. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 2 } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Re-add a user. next_index sees occupied = {1, 3}, so it assigns index 2. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Dave", "guest" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + { userIndex = 2, userName = "Dave", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "State-consistency: add users+credentials 1-3, deleteUser 2 (with credential), re-add user+credential reclaims index 2 cleanly", + function() + -- Populate three user and credential slots. + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }) + seed_credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, + }) + + -- Delete user at index 2. The handler finds a linked credential and injects + -- deleteCredential, which sends ClearPINCode to the lock. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 2 } }, + }) + expect_clear_pin_code(2) + test.wait_for_events() + + -- Lock acknowledges the deletion. + -- The legacy subdriver handles deletion notifications for this mock profile, so clear local + -- state directly to model the intended post-delete state before testing index reuse. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + assert(table_utils.delete_entry(mock_device, "users", 2) == constants.COMMAND_RESULT.SUCCESS) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, + }, + { visibility = { displayed = false } } + )) + ) + assert(table_utils.delete_entry(mock_device, "credentials", 2) == constants.COMMAND_RESULT.SUCCESS) + test.wait_for_events() + mock_device:set_field(constants.DRIVER_STATE.BUSY, false, {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, nil, {}) + + -- Re-add a user. next_index sees occupied = {1, 3} and assigns index 2. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Dave", "guest" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + { userIndex = 2, userName = "Dave", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Re-add a credential for the new user at slot 2. The old credential at + -- credentialIndex 2 was cleanly removed, so add_entry must succeed without + -- returning OCCUPIED or any stale-state error. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 2, "guest", "pin", "9999" } }, + }) + expect_set_pin_code(2, "9999") + test.wait_for_events() + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin", credentialName = "Guest 2" }, + }, + { visibility = { displayed = false } } + )) + ) + assert(table_utils.add_entry(mock_device, "credentials", { + userIndex = 2, + credentialIndex = 2, + credentialType = "pin", + credentialName = "Guest 2", + }) == constants.COMMAND_RESULT.SUCCESS) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua index 81ba1df2ad..532e1da1a9 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua @@ -1,15 +1,17 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" -local json = require "dkjson" local zw_test_utils = require "integration_test.zwave_test_utils" local t_utils = require "integration_test.utils" local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({version=1}) +local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local constants = require "st.zwave.constants" +local lock_utils = require "lock_utils.constants".DRIVER_STATE + +test.disable_startup_messages() local SAMSUNG_MANUFACTURER_ID = 0x022E local SAMSUNG_PRODUCT_TYPE = 0x0001 @@ -18,47 +20,53 @@ local SAMSUNG_PRODUCT_ID = 0x0001 local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock.yml"), + _provisioning_state = "TYPED", zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, zwave_product_type = SAMSUNG_PRODUCT_TYPE, - zwave_product_id = SAMSUNG_PRODUCT_ID + zwave_product_id = SAMSUNG_PRODUCT_ID, } ) local function test_init() test.mock_device.add_test_device(mock_device) end + test.set_test_init_function(test_init) -local function init_code_slot(slot_number, name, device) - local lock_codes = device.persistent_store[constants.LOCK_CODES] - if lock_codes == nil then - lock_codes = {} - device.persistent_store[constants.LOCK_CODES] = lock_codes - end - lock_codes[tostring(slot_number)] = name +local function added() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") end test.register_coroutine_test( "When the device is added an unlocked event should be sent", function() + added() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"}), { visibility = { displayed = false } })) - ) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( "Setting a user code name should be handled", function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + added() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( mock_device, @@ -89,95 +97,121 @@ test.register_coroutine_test( }) }) test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, + { visibility = { displayed = false } }) + ) ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end ) test.register_coroutine_test( "Notification about correctly added code should be handled", function() - mock_device.persistent_store["_code_state"] = {["setName2"] = "Code 2"} - test.socket.zwave:__queue_receive({ mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE - }) - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 failed", { state_change = true }))) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Notification about duplicated code should be handled", - function() - mock_device.persistent_store["_code_state"] = {["setName2"] = "Code 2"} + added() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + ) + test.wait_for_events() test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE + event = Notification.event.access_control.NEW_USER_CODE_ADDED, + event_parameter = string.char(1) }) }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged(2 .. " failed", { state_change = true }))) - end, - { - min_api_version = 17 - } + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Get({user_identifier = 1}) + ) + ) + end ) test.register_coroutine_test( "All user codes should be reported as deleted upon changing Master Code", function() - init_code_slot(0, "Master Code", mock_device) - init_code_slot(1, "Code 1", mock_device) - init_code_slot(2, "Code 2", mock_device) - init_code_slot(3, "Code 3", mock_device) - test.socket.zwave:__queue_receive({ + added() + mock_device:set_field("persistedUsers", { + { userIndex = 1, userName = "Code 1", userType = "guest" }, + { userIndex = 2, userName = "Code 2", userType = "guest" }, + { userIndex = 3, userName = "Code 3", userType = "guest" }, + }, { persist = true }) + mock_device:set_field("persistedCredentials", { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, + }, { persist = true }) + test.socket.capability:__queue_receive({ mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, - event_parameter = "" } - ) + { + capability = capabilities.lockUsers.ID, + command = "updateUser", + args = {1, "new name", "guest" } + }, }) test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) ) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({ + { userIndex = 1, userName = "new name", userType = "guest" }, + { userIndex = 2, userName = "Code 2", userType = "guest" }, + { userIndex = 3, userName = "Code 3", userType = "guest" } + }, + { visibility = { displayed = false } }) ) ) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, + event_parameter = "" } + ) + }) + test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { visibility = { displayed = false } }) ) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { visibility = { displayed = false } }) ) ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } }) - )) - end, - { - min_api_version = 17 - } + end ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua new file mode 100644 index 0000000000..81ba1df2ad --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua @@ -0,0 +1,183 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local json = require "dkjson" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" +local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) +local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) +local constants = require "st.zwave.constants" + +local SAMSUNG_MANUFACTURER_ID = 0x022E +local SAMSUNG_PRODUCT_TYPE = 0x0001 +local SAMSUNG_PRODUCT_ID = 0x0001 + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, + zwave_product_type = SAMSUNG_PRODUCT_TYPE, + zwave_product_id = SAMSUNG_PRODUCT_ID + } +) + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local function init_code_slot(slot_number, name, device) + local lock_codes = device.persistent_store[constants.LOCK_CODES] + if lock_codes == nil then + lock_codes = {} + device.persistent_store[constants.LOCK_CODES] = lock_codes + end + lock_codes[tostring(slot_number)] = name +end + +test.register_coroutine_test( + "When the device is added an unlocked event should be sent", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"}), { visibility = { displayed = false } })) + ) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a user code name should be handled", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_ADDED, + event_parameter = "" } + ) + }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Get({user_identifier = 1}) + ) + ) + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_code = "1234", + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS + }) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Notification about correctly added code should be handled", + function() + mock_device.persistent_store["_code_state"] = {["setName2"] = "Code 2"} + test.socket.zwave:__queue_receive({ mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE + }) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 failed", { state_change = true }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Notification about duplicated code should be handled", + function() + mock_device.persistent_store["_code_state"] = {["setName2"] = "Code 2"} + test.socket.zwave:__queue_receive({ mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_NOT_ADDED_DUE_TO_DUPLICATE_CODE + }) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged(2 .. " failed", { state_change = true }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "All user codes should be reported as deleted upon changing Master Code", + function() + init_code_slot(0, "Master Code", mock_device) + init_code_slot(1, "Code 1", mock_device) + init_code_slot(2, "Code 2", mock_device) + init_code_slot(3, "Code 3", mock_device) + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, + event_parameter = "" } + ) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("0 set", { data = { codeName = "Master Code"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 deleted", { data = { codeName = "Code 1"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("2 deleted", { data = { codeName = "Code 2"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("3 deleted", { data = { codeName = "Code 3"}, state_change = true }) + ) + ) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["0"] = "Master Code"} ), { visibility = { displayed = false } }) + )) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua index 38dabbd9dd..90d919c27e 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua @@ -1,18 +1,21 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local test = require "integration_test" local capabilities = require "st.capabilities" local zw = require "st.zwave" -local json = require "dkjson" local zw_test_utils = require "integration_test.zwave_test_utils" local t_utils = require "integration_test.utils" +local lock_utils = require "lock_utils.constants".DRIVER_STATE local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) +local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) + +test.disable_startup_messages() local SCHLAGE_MANUFACTURER_ID = 0x003B local SCHLAGE_PRODUCT_TYPE = 0x0002 @@ -33,10 +36,11 @@ local zwave_lock_endpoints = { local mock_device = test.mock_device.build_test_zwave_device( { profile = t_utils.get_profile_definition("base-lock.yml"), + _provisioning_state = "TYPED", zwave_endpoints = zwave_lock_endpoints, zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, zwave_product_type = SCHLAGE_PRODUCT_TYPE, - zwave_product_id = SCHLAGE_PRODUCT_ID + zwave_product_id = SCHLAGE_PRODUCT_ID, } ) @@ -45,13 +49,32 @@ local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} local function test_init() test.mock_device.add_test_device(mock_device) end + test.set_test_init_function(test_init) +local function added() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + + test.socket.zwave:__expect_send( + DoorLock:OperationGet({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + Battery:Get({}):build_test_tx(mock_device.id) + ) + test.socket.zwave:__expect_send( + UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) + ) + test.wait_for_events() + assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") +end + test.register_coroutine_test( "Setting a user code should result in the named code changed event firing", function() + added() test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( mock_device, @@ -69,41 +92,29 @@ test.register_coroutine_test( test.wait_for_events() test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.STATUS_NOT_AVAILABLE, user_code="0000\n\r"}) }) test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) - ) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Setting a code length should be handled", - function() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCodeLength", args = { 6 } } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Configuration:Set({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = 6, - size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size - }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, + { visibility = { displayed = false } }) + ) ) - ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + end ) test.register_coroutine_test( "Configuration report should be handled", function() + added() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -112,17 +123,18 @@ test.register_coroutine_test( }) }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)) + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) + ) + end ) test.register_coroutine_test( "Configuration report indicating code deletion should be handled", function() + added() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -131,8 +143,12 @@ test.register_coroutine_test( }) }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)) + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) ) + test.wait_for_events() test.socket.zwave:__queue_receive({ mock_device.id, Configuration:Report({ @@ -140,21 +156,34 @@ test.register_coroutine_test( configuration_value = 4 }) }) + test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), {visibility = {displayed = false}})) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { visibility = { displayed = false } }) + ) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(4)) + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { visibility = { displayed = false } }) + ) ) - end, - { - min_api_version = 17 - } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(4)) + ) + end ) test.register_coroutine_test( "User code report indicating master code is available should indicate code deletion", - function () + function() + added() test.socket.zwave:__queue_receive({ mock_device.id, UserCode:Report({ @@ -162,55 +191,28 @@ test.register_coroutine_test( user_id_status = UserCode.user_id_status.AVAILABLE }) }) + test.socket.capability:__set_channel_ordering("relaxed") test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), {visibility = {displayed = false}})) + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users({}, + { visibility = { displayed = false } }) + ) ) - end, - { - min_api_version = 17 - } -) - -local expect_reload_all_codes_messages = function() - test.socket.capability:__expect_send(mock_device:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) - )) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:UsersNumberGet({}) - )) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) - test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Get({ user_identifier = 1 }) - )) -end - -test.register_coroutine_test( - "Reload all codes should complete as expected", - function () - test.socket.capability:__queue_receive({ - mock_device.id, - { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {}, component = "main"} - }) - expect_reload_all_codes_messages() - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Configuration:Get({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number - }) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials({}, + { visibility = { displayed = false } }) ) ) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( "Device should send appropriate configuration messages", function() + added() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.zwave:__expect_send( zw_test_utils.zwave_test_build_send_command( @@ -230,15 +232,13 @@ test.register_coroutine_test( ) ) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( "Basic Sets should result in an Association remove", - function () + function() + added() test.socket.zwave:__queue_receive({ mock_device.id, Basic:Set({ @@ -257,10 +257,7 @@ test.register_coroutine_test( }) ) ) - end, - { - min_api_version = 17 - } + end ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua new file mode 100644 index 0000000000..38dabbd9dd --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua @@ -0,0 +1,266 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local test = require "integration_test" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local json = require "dkjson" +local zw_test_utils = require "integration_test.zwave_test_utils" +local t_utils = require "integration_test.utils" + +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) +local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) +local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) + +local SCHLAGE_MANUFACTURER_ID = 0x003B +local SCHLAGE_PRODUCT_TYPE = 0x0002 +local SCHLAGE_PRODUCT_ID = 0x0469 + +-- supported comand classes +local zwave_lock_endpoints = { + { + command_classes = { + {value = zw.BATTERY}, + {value = zw.DOOR_LOCK}, + {value = zw.USER_CODE}, + {value = zw.NOTIFICATION} + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, + zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, + zwave_product_type = SCHLAGE_PRODUCT_TYPE, + zwave_product_id = SCHLAGE_PRODUCT_ID + } +) + +local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Setting a user code should result in the named code changed event firing", + function() + test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCode", args = { 1, "1234", "test" } } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number}) + ) + ) + test.wait_for_events() + test.mock_time.advance_time(4.2) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) + ) + ) + test.wait_for_events() + test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.STATUS_NOT_AVAILABLE, user_code="0000\n\r"}) }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.codeChanged("1 set", { data = { codeName = "test"}, state_change = true })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Setting a code length should be handled", + function() + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "setCodeLength", args = { 6 } } }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Configuration:Set({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = 6, + size = SCHLAGE_LOCK_CODE_LENGTH_PARAM.size + }) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Configuration report should be handled", + function() + test.socket.zwave:__queue_receive({ + mock_device.id, + Configuration:Report({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = 6 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Configuration report indicating code deletion should be handled", + function() + test.socket.zwave:__queue_receive({ + mock_device.id, + Configuration:Report({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = 6 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(6)) + ) + test.socket.zwave:__queue_receive({ + mock_device.id, + Configuration:Report({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, + configuration_value = 4 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.codeLength(4)) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "User code report indicating master code is available should indicate code deletion", + function () + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:Report({ + user_identifier = 0, + user_id_status = UserCode.user_id_status.AVAILABLE + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.lockCodes(json.encode({}), {visibility = {displayed = false}})) + ) + end, + { + min_api_version = 17 + } +) + +local expect_reload_all_codes_messages = function() + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.lockCodes.lockCodes(json.encode({} ), { visibility = { displayed = false } }) + )) + test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:UsersNumberGet({}) + )) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) + test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:Get({ user_identifier = 1 }) + )) +end + +test.register_coroutine_test( + "Reload all codes should complete as expected", + function () + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCodes.ID, command = "reloadAllCodes", args = {}, component = "main"} + }) + expect_reload_all_codes_messages() + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Configuration:Get({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number + }) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Device should send appropriate configuration messages", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Configuration:Get({ + parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number + }) + ) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Association:Set({ + grouping_identifier = 2, + node_ids = {} + }) + ) + ) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Basic Sets should result in an Association remove", + function () + test.socket.zwave:__queue_receive({ + mock_device.id, + Basic:Set({ + value = 0x00 + }) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({})) + ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + Association:Remove({ + grouping_identifier = 1, + node_ids = {} + }) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua deleted file mode 100644 index 1eb2d093e5..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_code_migration.lua +++ /dev/null @@ -1,251 +0,0 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - --- Mock out globals -local test = require "integration_test" -local capabilities = require "st.capabilities" -local zw = require "st.zwave" ---- @type st.zwave.CommandClass.DoorLock -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) ---- @type st.zwave.CommandClass.Battery -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) ---- @type st.zwave.CommandClass.UserCode -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) -local t_utils = require "integration_test.utils" -local zw_test_utils = require "integration_test.zwave_test_utils" -local utils = require "st.utils" - -local mock_datastore = require "integration_test.mock_env_datastore" - -local json = require "dkjson" - -local zwave_lock_endpoints = { - { - command_classes = { - { value = zw.BATTERY }, - { value = zw.DOOR_LOCK }, - { value = zw.USER_CODE }, - { value = zw.NOTIFICATION } - } - } -} - -local lockCodes = { - ["1"] = "Zach", - ["2"] = "Steven" -} - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints, - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } - } -) - -local mock_device_no_data = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - data = {} - } -) - -local expect_reload_all_codes_messages = function(dev, lc) - test.socket.capability:__expect_send(dev:generate_test_message("main", - capabilities.lockCodes.lockCodes(json.encode(lc), { visibility = { displayed = false } }) - )) - test.socket.zwave:__expect_send( UserCode:UsersNumberGet({}):build_test_tx(dev.id) ) - test.socket.capability:__expect_send(dev:generate_test_message("main", capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } }))) - test.socket.zwave:__expect_send( UserCode:Get({ user_identifier = 1 }):build_test_tx(dev.id) ) -end - -test.register_coroutine_test( - "Device added data lock codes population", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device added without data should function", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - expect_reload_all_codes_messages(mock_device_no_data,{}) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device_no_data:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", nil) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode({})) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", nil) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device init after added shouldn't change the datastores", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device init after added with no data should update the datastores", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - -- This should happen as the data is empty at this point - expect_reload_all_codes_messages(mock_device_no_data, {}) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device_no_data, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device_no_data:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", nil) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode({})) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", nil) - test.socket.device_lifecycle():__queue_receive(mock_device_no_data:generate_info_changed( - { - data = { - lockCodes = json.encode(utils.deep_copy(lockCodes)) - } - } - )) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "init" }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device_no_data.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - - -test.register_coroutine_test( - "Device added data lock codes population, should not reload all codes", - function() - test.timer.__create_and_queue_test_time_advance_timer(31, "oneshot") - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "_lock_codes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - assert(mock_device.state_cache.main.lockCodes.lockCodes.value == json.encode(utils.deep_copy(lockCodes))) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.wait_for_events() - test.mock_time.advance_time(35) - -- Nothing should happen - end, - { - min_api_version = 17 - } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua similarity index 97% rename from drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua rename to drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua index c798feca08..66038d0760 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua @@ -33,10 +33,10 @@ local zwave_lock_endpoints = { } local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock-tamper.yml"), - zwave_endpoints = zwave_lock_endpoints - } + { + profile = t_utils.get_profile_definition("base-lock-tamper.yml"), + zwave_endpoints = zwave_lock_endpoints + } ) local function test_init() @@ -53,31 +53,6 @@ local expect_reload_all_codes_messages = function() test.socket.zwave:__expect_send( UserCode:Get({ user_identifier = 1 }):build_test_tx(mock_device.id) ) end -test.register_coroutine_test( - "When the device is added it should be set up and start reading codes", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - - expect_reload_all_codes_messages() - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - DoorLock:OperationGet({}) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Battery:Get({}) - ) - ) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) - end, - { - min_api_version = 17 - } -) - test.register_coroutine_test( "Door Lock Operation Reports should be handled", function() @@ -502,7 +477,7 @@ test.register_coroutine_test( Notification:Report({ notification_type = Notification.notification_type.ACCESS_CONTROL, event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, - event_parameter = "" + event_parameter = "\x01" }) } ) @@ -824,6 +799,12 @@ test.register_coroutine_test( Battery:Get({}) ) ) + test.socket.zwave:__expect_send( + zw_test_utils.zwave_test_build_send_command( + mock_device, + UserCode:UsersNumberGet({}) + ) + ) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_responses.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_responses.lua new file mode 100644 index 0000000000..71bf25209d --- /dev/null +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_responses.lua @@ -0,0 +1,393 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Tests for lock_handlers/zwave_responses.lua +-- Covers: +-- • user_code_report: sync-codes-from-lock flow (advances slot, completes sync) +-- • user_code_report: out-of-band ENABLED code creates user+credential entries +-- • user_code_event_handler (via Notification:Report): NEW_USER_CODE_ADDED out-of-band +-- • user_code_event_handler: multi-byte event_parameter (byte 3 carries user_id) +-- • door_operation_event_handler: KEYPAD_UNLOCK with user info lookup +-- • door_operation_event_handler: KEYPAD_UNLOCK with unknown credential +-- • door_operation_event_handler: AUTO_LOCK_LOCKED_OPERATION → locked/auto +-- • DoorLock:OperationReport DOOR_SECURED / DOOR_UNSECURED → locked / unlocked +-- • UserCode:UsersNumberReport → pinUsersSupported + totalUsersSupported +-- +-- Removed: alarm tests — z-wave alarm decoding is only in the legacy-handlers sub-driver +-- for non-SLGA devices; not applicable to the main SLGA driver path. +-- Removed: max/min PIN code length tests — no z-wave equivalent attribute. +-- Removed: NOT_FULLY_LOCKED lock_state test — no clean z-wave OperationReport equivalent. +-- Removed: NumberOfPINUsersSupported profile-migration tests — that logic lives in the +-- zigbee legacy sub-driver; users_number_report in z-wave only emits events. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zw = require "st.zwave" +local constants = require "lock_utils.constants" + +--- @type st.zwave.CommandClass.DoorLock +local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) +--- @type st.zwave.CommandClass.UserCode +local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) + +local zwave_lock_endpoints = { + { + command_classes = { + { value = zw.BATTERY }, + { value = zw.DOOR_LOCK }, + { value = zw.USER_CODE }, + { value = zw.NOTIFICATION }, + } + } +} + +local mock_device = test.mock_device.build_test_zwave_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zwave_endpoints = zwave_lock_endpoints, +}) + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) +end +test.set_test_init_function(test_init) + +-- ============================================================================ +-- user_code_report: sync codes from lock +-- During sync (CODES_FROM_LOCK busy state), ENABLED slots advance the index +-- without adding table entries; only AVAILABLE slots remove local entries. +-- ============================================================================ + +test.register_coroutine_test( + "user_code_report: sync advances to next code slot when slot is occupied", + function() + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.SYNC.CODES_FROM_LOCK, {}) + mock_device:set_field(constants.SYNC.CODE_INDEX, 1, {}) + + -- Receive UserCode:Report for slot 1 (occupied) + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS, + user_code = "1234", + }), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + + -- driver requests the next code slot. + test.socket.zwave:__expect_send( + UserCode:Get({ user_identifier = 2 }):build_test_tx(mock_device.id) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "user_code_report: sync completes when the last slot is reached", + function() + -- get_max_entries defaults to 20 when the attribute is missing + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.SYNC.CODES_FROM_LOCK, {}) + mock_device:set_field(constants.SYNC.CODE_INDEX, 20, {}) + + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:Report({ + user_identifier = 20, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS, + user_code = "1234", + }), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 20, userName = "Guest 20", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 20, credentialIndex = 20, credentialType = "pin", credentialName = "Guest 20" } }, + { visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + assert(mock_device:get_field(constants.SYNC.CODE_INDEX) == nil, + "CODE_INDEX must be nil after sync completes") + assert(mock_device:get_field(constants.DRIVER_STATE.BUSY) == false, + "BUSY must be false after sync completes") + end +) + +test.register_coroutine_test( + "user_code_report: out-of-band ENABLED code creates user and credential entries", + function() + -- command_in_progress == nil → out-of-band path + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:Report({ + user_identifier = 1, + user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS, + user_code = "1234", + }), + }) + + -- User entry added + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + -- Credential entry added + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- user_code_event_handler (ACCESS_CONTROL Notification:Report) +-- ============================================================================ + +test.register_coroutine_test( + "user_code_event: NEW_USER_CODE_ADDED out-of-band creates user and credential entries", + function() + -- event_parameter = single byte encodes user_id = 1 + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_ADDED, + event_parameter = string.char(1), + }), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "user_code_event: multi-byte event_parameter uses byte 3 to extract user_id", + function() + -- 3-byte event_parameter: bytes are (0, 0, 2) → byte 3 = 2 → user_id = 2 + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.NEW_USER_CODE_ADDED, + event_parameter = string.char(0, 0, 2), + }), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 2, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- door_operation_event_handler (ACCESS_CONTROL Notification:Report) +-- ============================================================================ + +test.register_coroutine_test( + "door_operation_event: KEYPAD_UNLOCK_OPERATION includes user info when credential exists", + function() + -- Seed persisted user/credential tables so the driver's lookup succeeds. + mock_device:set_field("persistedUsers", { + { userIndex = 1, userName = "John Doe", userType = "guest" }, + }, { persist = true }) + mock_device:set_field("persistedCredentials", { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + }, { persist = true }) + + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, + event_parameter = string.char(1), + }), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ + data = { + method = "keypad", + userIndex = 1, + userName = "John Doe", + userType = "guest", + }, + }) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "door_operation_event: KEYPAD_UNLOCK_OPERATION with unknown credential sets userIndex from event_parameter", + function() + -- No credential seeded for slot 99 + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.KEYPAD_UNLOCK_OPERATION, + event_parameter = string.char(99), + }), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ + data = { + method = "keypad", + userIndex = 99, + }, + }) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "door_operation_event: AUTO_LOCK_LOCKED_OPERATION emits locked with auto method", + function() + test.socket.zwave:__queue_receive({ + mock_device.id, + Notification:Report({ + notification_type = Notification.notification_type.ACCESS_CONTROL, + event = Notification.event.access_control.AUTO_LOCK_LOCKED_OPERATION, + event_parameter = "", + }), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked({ data = { method = "auto" } }) + ) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- DoorLock:OperationReport → lock state +-- Removed: NOT_FULLY_LOCKED — no clean z-wave OperationReport equivalent. +-- ============================================================================ + +test.register_coroutine_test( + "OperationReport: DOOR_SECURED emits locked event", + function() + test.socket.zwave:__queue_receive({ + mock_device.id, + DoorLock:OperationReport({ door_lock_mode = DoorLock.door_lock_mode.DOOR_SECURED }), + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.locked()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "OperationReport: DOOR_UNSECURED emits unlocked event", + function() + test.socket.zwave:__queue_receive({ + mock_device.id, + DoorLock:OperationReport({ door_lock_mode = DoorLock.door_lock_mode.DOOR_UNSECURED }), + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- users_number_report: UserCode:UsersNumberReport +-- ============================================================================ + +test.register_coroutine_test( + "users_number_report: emits pinUsersSupported and totalUsersSupported", + function() + test.socket.zwave:__queue_receive({ + mock_device.id, + UserCode:UsersNumberReport({ supported_users = 20 }), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.totalUsersSupported( + 20, { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.pinUsersSupported( + 20, { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua index 7bb54f23f2..42a80c6f66 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/can_handle.lua @@ -1,11 +1,14 @@ --- Copyright 2025 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local function can_handle_v1_alarm(opts, driver, device, cmd, ...) - if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then - return true, require("zwave-alarm-v1-lock") +return function(opts, driver, device, cmd) + local consts = require("lock_utils.constants") + local slga_migrated = device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) or false + if slga_migrated then + if opts.dispatcher_class == "ZwaveDispatcher" and cmd ~= nil and cmd.version ~= nil and cmd.version == 1 then + local subdriver = require("zwave-alarm-v1-lock") + return true, subdriver + end end return false end - -return can_handle_v1_alarm diff --git a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua index d7c862f22a..fa9836498b 100644 --- a/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua +++ b/drivers/SmartThings/zwave-lock/src/zwave-alarm-v1-lock/init.lua @@ -1,7 +1,6 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local capabilities = require "st.capabilities" --- @type st.zwave.CommandClass local cc = require "st.zwave.CommandClass" @@ -9,9 +8,10 @@ local cc = require "st.zwave.CommandClass" local Alarm = (require "st.zwave.CommandClass.Alarm")({ version = 1 }) --- @type st.zwave.CommandClass.Battery local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) ---- @type st.zwave.defaults.lockCodes -local lock_code_defaults = require "st.zwave.defaults.lockCodes" -local json = require "dkjson" + +local consts = require "lock_utils.constants" +local lock_utils = require "lock_utils.utils" +local tables = require "lock_utils.tables" local METHOD = { KEYPAD = "keypad", @@ -20,12 +20,6 @@ local METHOD = { AUTO = "auto" } ---- Determine whether the passed command is a V1 alarm command ---- ---- @param driver st.zwave.Driver ---- @param device st.zwave.Device ---- @return boolean true if the device is smoke co alarm - --- Default handler for alarm command class reports, these were largely OEM-defined --- --- This converts alarm V1 reports to correct lock events @@ -35,84 +29,66 @@ local METHOD = { --- @param cmd st.zwave.CommandClass.Alarm.Report local function alarm_report_handler(driver, device, cmd) local alarm_type = cmd.args.alarm_type - local event = nil - local lock_codes = lock_code_defaults.get_lock_codes(device) - local code_id = nil - if (cmd.args.alarm_level ~= nil) then - code_id = tostring(cmd.args.alarm_level) - end + local credential_index = cmd.args.alarm_level + local event if (alarm_type == 9 or alarm_type == 17) then event = capabilities.lock.lock.unknown() elseif (alarm_type == 16 or alarm_type == 19) then event = capabilities.lock.lock.unlocked() - if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + if (credential_index ~= nil) then + local credential = tables.find_entry(device, "credentials", credential_index) + local user_id = credential and credential.userIndex or nil + event.data = { userIndex = user_id, method = METHOD.KEYPAD} end elseif (alarm_type == 18) then event = capabilities.lock.lock.locked() - if (device:supports_capability(capabilities.lockCodes) and code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - event.data = {codeId = code_id, codeName = code_name, method = METHOD.KEYPAD} + if (credential_index ~= nil) then + local credential = tables.find_entry(device, "credentials", credential_index) + local user_id = credential and credential.userIndex or nil + event.data = { userIndex = user_id, method = METHOD.KEYPAD} end elseif (alarm_type == 21) then event = capabilities.lock.lock.locked() - if (cmd.args.alarm_level == 2) then - event["data"] = {method = METHOD.MANUAL} - else - event["data"] = {method = METHOD.KEYPAD} - end + event.data = {method = (cmd.args.alarm_level == 2) and METHOD.MANUAL or METHOD.KEYPAD} elseif (alarm_type == 22) then event = capabilities.lock.lock.unlocked() - event["data"] = {method = METHOD.MANUAL} + event.data = {method = METHOD.MANUAL} elseif (alarm_type == 23) then event = capabilities.lock.lock.unknown() - event["data"] = {method = METHOD.COMMAND} + event.data = {method = METHOD.COMMAND} elseif (alarm_type == 24) then event = capabilities.lock.lock.locked() - event["data"] = {method = METHOD.COMMAND} + event.data = {method = METHOD.COMMAND} elseif (alarm_type == 25) then event = capabilities.lock.lock.unlocked() - event["data"] = {method = METHOD.COMMAND} + event.data = {method = METHOD.COMMAND} elseif (alarm_type == 26) then event = capabilities.lock.lock.unknown() - event["data"] = {method = METHOD.AUTO} + event.data = {method = METHOD.AUTO} elseif (alarm_type == 27) then event = capabilities.lock.lock.locked() - event["data"] = {method = METHOD.AUTO} + event.data = {method = METHOD.AUTO} elseif (alarm_type == 32) then - -- all user codes deleted - for code_id, _ in pairs(lock_codes) do - lock_code_defaults.code_deleted(device, code_id) - end - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) + -- all credentials have been deleted + tables.delete_all_entries(device, "credentials") + tables.delete_all_entries(device, "users") elseif (alarm_type == 33) then - -- user code deleted - if (code_id ~= nil) then - lock_code_defaults.clear_code_state(device, code_id) - if (lock_codes[code_id] ~= nil) then - lock_code_defaults.code_deleted(device, code_id) - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(lock_code_defaults.get_lock_codes(device)), { visibility = { displayed = false } })) - end - end + -- credential has been deleted + lock_utils.delete_credential_report_helper(device, credential_index) + elseif (alarm_type == 13 or alarm_type == 112) then -- user code changed/set - if (code_id ~= nil) then - local code_name = lock_code_defaults.get_code_name(device, code_id) - local change_type = lock_code_defaults.get_change_type(device, code_id) - local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. change_type, { state_change = true }) - code_changed_event["data"] = { codeName = code_name} - lock_code_defaults.code_set_event(device, code_id, code_name) - lock_code_defaults.clear_code_state(device, code_id) - device:emit_event(code_changed_event) - end + lock_utils.set_credential_report_helper(device, credential_index) + elseif (alarm_type == 34 or alarm_type == 113) then - -- duplicate lock code - if (code_id ~= nil) then - local code_changed_event = capabilities.lockCodes.codeChanged(code_id .. lock_code_defaults.CHANGE_TYPE.FAILED, { state_change = true }) - lock_code_defaults.clear_code_state(device, code_id) - device:emit_event(code_changed_event) + -- duplicate lock code. Log duplicate error + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + if command_in_progress then + lock_utils.emit_command_result(device, capabilities.lockCredentials, + command_in_progress, consts.COMMAND_RESULT.DUPLICATE) + lock_utils.clear_busy_state(device) end + elseif (alarm_type == 130) then -- batteries replaced if (device:is_cc_supported(cc.BATTERY)) then @@ -146,7 +122,7 @@ local zwave_lock = { } }, NAME = "Z-Wave lock alarm V1", - can_handle = require("zwave-alarm-v1-lock.can_handle"), + can_handle = require("zwave-alarm-v1-lock.can_handle") } return zwave_lock From ec88723f065abc3edae835b0c385947513e12b78 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:52:19 -0500 Subject: [PATCH 022/102] Zigbee Lock: Support SLGA Migration (#2956) --- .../zigbee-lock/profiles/base-lock.yml | 4 + .../src/bad-battery-reporter/can_handle.lua | 13 + .../fingerprints.lua | 4 +- .../init.lua | 8 +- drivers/SmartThings/zigbee-lock/src/init.lua | 515 +++---------- .../src/legacy-handlers/can_handle.lua | 12 + .../zigbee-lock/src/legacy-handlers/init.lua | 495 ++++++++++++ .../src/legacy-handlers/legacy_lock_utils.lua | 89 +++ .../lock-without-codes/can_handle.lua | 13 + .../lock-without-codes/fingerprints.lua | 0 .../lock-without-codes/init.lua | 2 +- .../src/legacy-handlers/sub_drivers.lua | 10 + .../yale-fingerprint-lock/can_handle.lua | 13 + .../yale-fingerprint-lock/init.lua | 27 + .../{ => legacy-handlers}/yale/can_handle.lua | 2 +- .../src/{ => legacy-handlers}/yale/init.lua | 5 +- .../src/lock-without-codes/can_handle.lua | 14 - .../src/lock_handlers/capabilities.lua | 248 ++++++ .../src/lock_handlers/zigbee_responses.lua | 438 +++++++++++ .../zigbee-lock/src/lock_utils.lua | 87 --- .../zigbee-lock/src/lock_utils/constants.lua | 47 ++ .../zigbee-lock/src/lock_utils/tables.lua | 268 +++++++ .../zigbee-lock/src/lock_utils/utils.lua | 66 ++ .../zigbee-lock/src/samsungsds/init.lua | 88 ++- .../zigbee-lock/src/sub_drivers.lua | 4 +- ...rter.lua => test_bad_battery_reporter.lua} | 38 + ...st_generic_fingerprint_profile_update.lua} | 13 + .../src/test/test_init_lifecycle_handlers.lua | 484 ++++++++++++ .../test/test_lock_code_slga_migration.lua | 76 ++ .../test/test_lock_credentials_commands.lua | 578 ++++++++++++++ .../src/test/test_lock_pre_configured.lua | 653 ++++++++++++++++ .../src/test/test_lock_programming_events.lua | 574 ++++++++++++++ .../zigbee-lock/src/test/test_lock_tables.lua | 728 ++++++++++++++++++ .../src/test/test_lock_users_commands.lua | 654 ++++++++++++++++ .../test/test_zigbee_lock_code_migration.lua | 248 ------ ...e_lock.lua => test_zigbee_lock_legacy.lua} | 6 +- .../src/test/test_zigbee_responses.lua | 566 ++++++++++++++ .../src/test/test_zigbee_samsungsds.lua | 15 + .../test_zigbee_yale-bad-battery-reporter.lua | 41 - .../test_zigbee_yale-fingerprint-lock.lua | 41 - .../test_zigbee_yale_fingerprint-lock.lua | 78 ++ ...e_yale.lua => test_zigbee_yale_legacy.lua} | 0 .../src/yale-fingerprint-lock/can_handle.lua | 3 + .../src/yale-fingerprint-lock/init.lua | 8 +- .../zigbee-lock/src/yale/sub_drivers.lua | 8 - .../yale-bad-battery-reporter/can_handle.lua | 14 - 46 files changed, 6421 insertions(+), 877 deletions(-) create mode 100644 drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/can_handle.lua rename drivers/SmartThings/zigbee-lock/src/{yale/yale-bad-battery-reporter => bad-battery-reporter}/fingerprints.lua (79%) rename drivers/SmartThings/zigbee-lock/src/{yale/yale-bad-battery-reporter => bad-battery-reporter}/init.lua (75%) create mode 100644 drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/legacy-handlers/legacy_lock_utils.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/can_handle.lua rename drivers/SmartThings/zigbee-lock/src/{ => legacy-handlers}/lock-without-codes/fingerprints.lua (100%) rename drivers/SmartThings/zigbee-lock/src/{ => legacy-handlers}/lock-without-codes/init.lua (96%) create mode 100644 drivers/SmartThings/zigbee-lock/src/legacy-handlers/sub_drivers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/init.lua rename drivers/SmartThings/zigbee-lock/src/{ => legacy-handlers}/yale/can_handle.lua (85%) rename drivers/SmartThings/zigbee-lock/src/{ => legacy-handlers}/yale/init.lua (97%) delete mode 100644 drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock_handlers/capabilities.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock_handlers/zigbee_responses.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/lock_utils.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock_utils/constants.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock_utils/tables.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/lock_utils/utils.lua rename drivers/SmartThings/zigbee-lock/src/test/{test_yale_fingerprint_bad_battery_reporter.lua => test_bad_battery_reporter.lua} (59%) rename drivers/SmartThings/zigbee-lock/src/test/{test_generic_lock_migration.lua => test_generic_fingerprint_profile_update.lua} (56%) create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_init_lifecycle_handlers.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_lock_code_slga_migration.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_lock_credentials_commands.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_lock_pre_configured.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_lock_programming_events.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_lock_tables.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_lock_users_commands.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua rename drivers/SmartThings/zigbee-lock/src/test/{test_zigbee_lock.lua => test_zigbee_lock_legacy.lua} (99%) create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_responses.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua create mode 100644 drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_fingerprint-lock.lua rename drivers/SmartThings/zigbee-lock/src/test/{test_zigbee_yale.lua => test_zigbee_yale_legacy.lua} (100%) delete mode 100644 drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua delete mode 100644 drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua diff --git a/drivers/SmartThings/zigbee-lock/profiles/base-lock.yml b/drivers/SmartThings/zigbee-lock/profiles/base-lock.yml index 159e6939f3..c9c98898c6 100644 --- a/drivers/SmartThings/zigbee-lock/profiles/base-lock.yml +++ b/drivers/SmartThings/zigbee-lock/profiles/base-lock.yml @@ -6,6 +6,10 @@ components: version: 1 - id: lockCodes version: 1 + - id: lockCredentials + version: 1 + - id: lockUsers + version: 1 - id: battery version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/can_handle.lua new file mode 100644 index 0000000000..7d7d566118 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/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 fingerprints = require("bad-battery-reporter.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("bad-battery-reporter") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/fingerprints.lua similarity index 79% rename from drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua rename to drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/fingerprints.lua index cbb7c3404f..3024aeeace 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/fingerprints.lua +++ b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/fingerprints.lua @@ -1,7 +1,7 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 -local BAD_YALE_LOCK_FINGERPRINTS = { +local BAD_BATTERY_REPORTING_LOCK_FINGERPRINTS = { { mfr = "Yale", model = "YRD220/240 TSDB" }, { mfr = "Yale", model = "YRL220 TS LL" }, { mfr = "Yale", model = "YRD210 PB DB" }, @@ -10,4 +10,4 @@ local BAD_YALE_LOCK_FINGERPRINTS = { { mfr = "ASSA ABLOY iRevo", model = "06ffff2027" } } -return BAD_YALE_LOCK_FINGERPRINTS +return BAD_BATTERY_REPORTING_LOCK_FINGERPRINTS diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/init.lua similarity index 75% rename from drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua rename to drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/init.lua index 3b77f32563..5fbdabd1b7 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/bad-battery-reporter/init.lua @@ -11,8 +11,8 @@ local battery_report_handler = function(driver, device, value) device:emit_event(capabilities.battery.battery(value.value)) end -local bad_yale_driver = { - NAME = "YALE BAD Lock Driver", +local bad_battery_reporter_driver = { + NAME = "Bad Battery Reporter Driver", zigbee_handlers = { attr = { [clusters.PowerConfiguration.ID] = { @@ -20,7 +20,7 @@ local bad_yale_driver = { } } }, - can_handle = require("yale.yale-bad-battery-reporter.can_handle"), + can_handle = require("bad-battery-reporter.can_handle") } -return bad_yale_driver +return bad_battery_reporter_driver diff --git a/drivers/SmartThings/zigbee-lock/src/init.lua b/drivers/SmartThings/zigbee-lock/src/init.lua index 1ac2598e2f..b8b290af4d 100644 --- a/drivers/SmartThings/zigbee-lock/src/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/init.lua @@ -1,450 +1,159 @@ --- Copyright 2022 SmartThings, Inc. +-- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - --- Zigbee Driver utilities +local ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" local device_management = require "st.zigbee.device_management" -local ZigbeeDriver = require "st.zigbee" - --- Zigbee Spec Utils -local clusters = require "st.zigbee.zcl.clusters" -local Alarm = clusters.Alarms -local LockCluster = clusters.DoorLock -local PowerConfiguration = clusters.PowerConfiguration - --- Capabilities -local capabilities = require "st.capabilities" -local Battery = capabilities.battery -local Lock = capabilities.lock -local LockCodes = capabilities.lockCodes - --- Enums -local UserStatusEnum = LockCluster.types.DrlkUserStatus -local UserTypeEnum = LockCluster.types.DrlkUserType -local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode - -local socket = require "cosock.socket" -local lock_utils = require "lock_utils" - -local DELAY_LOCK_EVENT = "_delay_lock_event" -local MAX_DELAY = 10 - -local reload_all_codes = function(driver, device, command) - -- starts at first user code index then iterates through all lock codes as they come in - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) == nil) then - device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) == nil) then - device:send(LockCluster.attributes.MinPINCodeLength:read(device)) - end - if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) == nil) then - device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) - end - if (device:get_field(lock_utils.CHECKING_CODE) == nil) then device:set_field(lock_utils.CHECKING_CODE, 0) end - device:emit_event(LockCodes.scanCodes("Scanning", { visibility = { displayed = false } })) - device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) -end - -local refresh = function(driver, device, cmd) - device:refresh() - device:send(LockCluster.attributes.LockState:read(device)) - device:send(Alarm.attributes.AlarmCount:read(device)) - -- we can't determine from fingerprints if devices support lock codes, so - -- here in the driver we'll do a check once to see if the device responds here - -- and if it does, we'll switch it to a profile with lock codes - if not device:supports_capability_by_id(LockCodes.ID) and not device:get_field(lock_utils.CHECKED_CODE_SUPPORT) then - device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) - -- we won't make this value persist because it's not that important - device:set_field(lock_utils.CHECKED_CODE_SUPPORT, true) - end -end - -local do_configure = function(self, device) - device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) - device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) - - device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) - device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) - - device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) - device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) - - -- Don't send a reload all codes if this is a part of migration - if device.data.lockCodes == nil or device:get_field(lock_utils.MIGRATION_RELOAD_SKIPPED) == true then - device.thread:call_with_delay(2, function(d) - self:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - }) - end) - else - device:set_field(lock_utils.MIGRATION_RELOAD_SKIPPED, true, { persist = true }) +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local consts = require "lock_utils.constants" +local lock_utils = require "lock_utils.utils" +local table_utils = require "lock_utils.tables" +local zigbee_handlers = require "lock_handlers.zigbee_responses" +local capability_handlers = require "lock_handlers.capabilities" + +local LockLifecycle = {} + +function LockLifecycle.device_added(driver, device) + if device:supports_capability(capabilities.lockCodes) and device._provisioning_state == "TYPED" then + -- set the migrated field to true so new devices use lockCredentials/lockUsers from the start. + -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, + -- and should be migrated manually by the user. + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) -- persist the migration event in the datastore end + -- set initial state + driver:inject_capability_command(device, { + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} + }) end -local alarm_handler = function(driver, device, zb_mess) - local ALARM_REPORT = { - [0] = Lock.lock.unknown(), - [1] = Lock.lock.unknown(), - -- Events 16-19 are low battery events, but are presented as descriptionText only - } - if (ALARM_REPORT[zb_mess.body.zcl_body.alarm_code.value] ~= nil) then - device:emit_event(ALARM_REPORT[zb_mess.body.zcl_body.alarm_code.value]) +function LockLifecycle.init(driver, device) + -- Restore users/credentials capability state from the persistent store in case + -- the capability state cache was wiped since the last driver run. + table_utils.restore_from_persistent_store(device) + + local lock_pins_supported_by_profile = device:supports_capability(capabilities.lockCodes) + if lock_pins_supported_by_profile and device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) == true then + -- ensure lockCodes capability state is reflected correctly for already migrated devices + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:emit_event(capabilities.lockCredentials.supportedCredentials({ consts.CRED_TYPE_PIN }, { visibility = { displayed = false } })) + elseif not lock_pins_supported_by_profile then + -- generically fingerprinted profiles do not have any codes/users/credentials capabilities. + -- We should check its PIN users if it should be re-profiled. + device:send(clusters.DoorLock.attributes.NumberOfPINUsersSupported:read(device)) end end -local get_pin_response_handler = function(driver, device, zb_mess) - local event = LockCodes.codeChanged("", { state_change = true }) - local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) - event.data = {codeName = lock_utils.get_code_name(device, code_slot)} - if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then - -- Code slot is occupied - event.value = code_slot .. lock_utils.get_change_type(device, code_slot) - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = event.data.codeName - device:emit_event(event) - lock_utils.lock_codes_event(device, lock_codes) - lock_utils.reset_code_state(device, code_slot) - else - -- Code slot is unoccupied - if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then - -- Code has been deleted - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) - else - -- Code is unset - event.value = code_slot .. " unset" - device:emit_event(event) - end - end +function LockLifecycle.do_configure(driver, device) + device:send(device_management.build_bind_request(device, clusters.PowerConfiguration.ID, driver.environment_info.hub_zigbee_eui)) + device:send(clusters.PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) - code_slot = tonumber(code_slot) - if (code_slot == device:get_field(lock_utils.CHECKING_CODE)) then - -- the code we're checking has arrived - local last_slot = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - 1 - if (code_slot >= last_slot) then - device:emit_event(LockCodes.scanCodes("Complete", { visibility = { displayed = false } })) - device:set_field(lock_utils.CHECKING_CODE, nil) - else - local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 - device:set_field(lock_utils.CHECKING_CODE, checkingCode) - device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) - end - end -end + device:send(device_management.build_bind_request(device, clusters.DoorLock.ID, driver.environment_info.hub_zigbee_eui)) + device:send(clusters.DoorLock.attributes.LockState:configure_reporting(device, 0, 3600, 0)) -local programming_event_handler = function(driver, device, zb_mess) - local event = LockCodes.codeChanged("", { state_change = true }) - local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) - event.data = {} - if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then - -- Master code changed - event.value = "0 set" - event.data = {codeName = "Master Code"} - device:emit_event(event) - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then - if (zb_mess.body.zcl_body.user_id.value == 0xFF) then - -- All codes deleted - for cs, _ in pairs(lock_utils.get_lock_codes(device)) do - lock_utils.code_deleted(device, cs) - end - lock_utils.lock_codes_event(device, {}) - else - -- One code deleted - if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) - end - end - elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or - zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then - -- Code added or changed - local change_type = lock_utils.get_change_type(device, code_slot) - local code_name = lock_utils.get_code_name(device, code_slot) - event.value = code_slot .. change_type - event.data = {codeName = code_name} - device:emit_event(event) - if (change_type == " set") then - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = code_name - lock_utils.lock_codes_event(device, lock_codes) - end - end -end + device:send(device_management.build_bind_request(device, clusters.Alarms.ID, driver.environment_info.hub_zigbee_eui)) + device:send(clusters.Alarms.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) -local handle_max_codes = function(driver, device, value) - if value.value ~= 0 then - -- Here's where we'll end up if we queried a lock whose profile does not have lock codes, - -- but it gave us a non-zero number of pin users, so we want to switch the profile - if not device:supports_capability_by_id(LockCodes.ID) then - device:try_update_metadata({profile = "base-lock"}) -- switch to a lock with codes - lock_utils.populate_state_from_data(device) -- if this was a migrated device, try to migrate the lock codes - if not device:get_field(lock_utils.MIGRATION_COMPLETE) then -- this means we didn't find any pre-migration lock codes - -- so we'll load them manually - driver:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - }) - end - end - device:emit_event(LockCodes.maxCodes(value.value, { visibility = { displayed = false } })) + if device:supports_capability(capabilities.lockCredentials) then + -- ensure our user/credential state is accurate to the current device state + device.thread:call_with_delay(15, function() lock_utils.sync_device_state(device) end) end end -local handle_max_code_length = function(driver, device, value) - device:emit_event(LockCodes.maxCodeLength(value.value, { visibility = { displayed = false } })) -end - -local handle_min_code_length = function(driver, device, value) - device:emit_event(LockCodes.minCodeLength(value.value, { visibility = { displayed = false } })) -end - -local update_codes = function(driver, device, command) - local delay = 0 - -- args.codes is json - for name, code in pairs(command.args.codes) do - -- these seem to come in the format "code[slot#]: code" - local code_slot = tonumber(string.gsub(name, "code", ""), 10) - if (code_slot ~= nil) then - if (code ~= nil and (code ~= "0" and code ~= "")) then - device.thread:call_with_delay(delay, function () - device:send(LockCluster.server.commands.SetPINCode(device, - code_slot, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - code)) - end) - delay = delay + 2 - else - device.thread:call_with_delay(delay, function () - device:send(LockCluster.server.commands.ClearPINCode(device, code_slot)) - end) - delay = delay + 2 - end - device.thread:call_with_delay(delay, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, code_slot)) - end) - delay = delay + 2 +function LockLifecycle.info_changed(driver, device, event, args) + local profile_switched = device.profile.id ~= args.old_st_store.profile.id + if profile_switched and device:supports_capability(capabilities.lockCodes) then + -- ensure all slga migration steps are run, and that the latest device state is synced to the driver. + device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + if device:supports_capability(capabilities.lockCredentials) then + device:emit_event(capabilities.lockCredentials.supportedCredentials({ consts.CRED_TYPE_PIN }, { visibility = { displayed = false } })) end - end -end - -local delete_code = function(driver, device, command) - device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) - device:send(LockCluster.server.commands.ClearPINCode(device, command.args.codeSlot)) - device.thread:call_with_delay(2, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) - end) -end - -local request_code = function(driver, device, command) - device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) -end - -local set_code = function(driver, device, command) - if (command.args.codePIN == "") then + -- ensure all requisite initial state is set driver:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.nameSlot.NAME, - args = {command.args.codeSlot, command.args.codeName} + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} }) - else - device:send(LockCluster.server.commands.SetPINCode(device, - command.args.codeSlot, - UserStatusEnum.OCCUPIED_ENABLED, - UserTypeEnum.UNRESTRICTED, - command.args.codePIN) - ) - if (command.args.codeName ~= nil) then - -- wait for confirmation from the lock to commit this to memory - -- Groovy driver has a lot more info passed here as a description string, may need to be investigated - local codeState = device:get_field(lock_utils.CODE_STATE) or {} - codeState["setName"..command.args.codeSlot] = command.args.codeName - device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) - end - - device.thread:call_with_delay(4, function(d) - device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) - end) + -- ensure our user/credential state is accurate to the current device state + device.thread:call_with_delay(15, function() lock_utils.sync_device_state(device) end) end end -local name_slot = function(driver, device, command) - local code_slot = tostring(command.args.codeSlot) - local lock_codes = lock_utils.get_lock_codes(device) - if (lock_codes[code_slot] ~= nil) then - lock_codes[code_slot] = command.args.codeName - device:emit_event(LockCodes.codeChanged(code_slot .. " renamed", { state_change = true })) - lock_utils.lock_codes_event(device, lock_codes) +function LockLifecycle.driver_switched(driver, device) + if device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME) == true then + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) end -end - -local function device_added(driver, device) - lock_utils.populate_state_from_data(device) - - driver:inject_capability_command(device, { - capability = capabilities.refresh.ID, - command = capabilities.refresh.commands.refresh.NAME, - args = {} - }) -end - -local function init(driver, device) - lock_utils.populate_state_from_data(device) - -- temp fix before this can be changed to non-persistent - device:set_field(lock_utils.CODE_STATE, nil, { persist = true }) -end - --- The following two functions are from the lock defaults. They are in the base driver temporarily --- until the fix is widely released in the lua libs -local lock_state_handler = function(driver, device, value, zb_rx) - local attr = capabilities.lock.lock - local LOCK_STATE = { - [value.NOT_FULLY_LOCKED] = attr.unknown(), - [value.LOCKED] = attr.locked(), - [value.UNLOCKED] = attr.unlocked(), - [value.UNDEFINED] = attr.unknown(), - } - - -- this is where we decide whether or not we need to delay our lock event because we've - -- observed it coming before the event (or we're starting to compute the timer) - local delay = device:get_field(DELAY_LOCK_EVENT) or 100 - if (delay < MAX_DELAY) then - device.thread:call_with_delay(delay+.5, function () - device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, LOCK_STATE[value.value] or attr.unknown()) - end) - else - device:set_field(DELAY_LOCK_EVENT, socket.gettime()) - device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, LOCK_STATE[value.value] or attr.unknown()) - end -end - -local lock_operation_event_handler = function(driver, device, zb_rx) - local event_code = zb_rx.body.zcl_body.operation_event_code.value - local source = zb_rx.body.zcl_body.operation_event_source.value - local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" - local METHOD = { - [0] = "keypad", - [1] = "command", - [2] = "manual", - [3] = "rfid", - [4] = "fingerprint", - [5] = "bluetooth" - } - local STATUS = { - [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), - [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), - [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() - } - local event = STATUS[event_code] - if (event ~= nil) then - event["data"] = {} - if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or - event_code == OperationEventCode.SCHEDULE_LOCK or - event_code == OperationEventCode.SCHEDULE_UNLOCK - ) then - event.data.method = "auto" - else - event.data.method = METHOD[source] - end - if (source == 0 and device:supports_capability_by_id(capabilities.lockCodes.ID)) then --keypad - local code_id = zb_rx.body.zcl_body.user_id.value - local code_name = "Code "..code_id - local lock_codes = device:get_field("lockCodes") - if (lock_codes ~= nil and - lock_codes[code_id] ~= nil) then - code_name = lock_codes[code_id] - end - event.data = {method = METHOD[0], codeId = code_id .. "", codeName = code_name} - end - - -- if this is an event corresponding to a recently-received attribute report, we - -- want to set our delay timer for future lock attribute report events - if device:get_latest_state( - device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), - capabilities.lock.ID, - capabilities.lock.lock.ID) == event.value.value then - local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 - local time_diff = socket.gettime() - preceding_event_time - if time_diff < MAX_DELAY then - device:set_field(DELAY_LOCK_EVENT, time_diff) - end - end - - device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) - end -end - -local function lock(driver, device, command) - device:send_to_component(command.component, LockCluster.server.commands.LockDoor(device)) -end - -local function unlock(driver, device, command) - device:send_to_component(command.component, LockCluster.server.commands.UnlockDoor(device)) + device:try_update_metadata({ provisioning_state = "PROVISIONED" }) end local zigbee_lock_driver = { - supported_capabilities = { - Lock, - LockCodes, - Battery, + lifecycle_handlers = { + added = LockLifecycle.device_added, + init = LockLifecycle.init, + doConfigure = LockLifecycle.do_configure, + driver_switched = LockLifecycle.driver_switched, + infoChanged = LockLifecycle.info_changed, }, zigbee_handlers = { cluster = { - [Alarm.ID] = { - [Alarm.client.commands.Alarm.ID] = alarm_handler + [clusters.Alarms.ID] = { + [clusters.Alarms.client.commands.Alarm.ID] = zigbee_handlers.alarm }, - [LockCluster.ID] = { - [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, - [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, - [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler + [clusters.DoorLock.ID] = { + [clusters.DoorLock.client.commands.ClearAllPINCodesResponse.ID] = zigbee_handlers.clear_all_pin_codes_response, + [clusters.DoorLock.client.commands.ClearPINCodeResponse.ID] = zigbee_handlers.clear_pin_code_response, + [clusters.DoorLock.client.commands.GetPINCodeResponse.ID] = zigbee_handlers.get_pin_code_response, + [clusters.DoorLock.client.commands.ProgrammingEventNotification.ID] = zigbee_handlers.programming_event_notification, + [clusters.DoorLock.client.commands.OperatingEventNotification.ID] = zigbee_handlers.operating_event_notification, + [clusters.DoorLock.client.commands.SetPINCodeResponse.ID] = zigbee_handlers.set_pin_code_response, } }, attr = { - [LockCluster.ID] = { - [LockCluster.attributes.LockState.ID] = lock_state_handler, - [LockCluster.attributes.MaxPINCodeLength.ID] = handle_max_code_length, - [LockCluster.attributes.MinPINCodeLength.ID] = handle_min_code_length, - [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes + [clusters.DoorLock.ID] = { + [clusters.DoorLock.attributes.LockState.ID] = zigbee_handlers.lock_state, + [clusters.DoorLock.attributes.MaxPINCodeLength.ID] = zigbee_handlers.max_pin_code_length, + [clusters.DoorLock.attributes.MinPINCodeLength.ID] = zigbee_handlers.min_pin_code_length, + [clusters.DoorLock.attributes.NumberOfPINUsersSupported.ID] = zigbee_handlers.number_of_pin_users_supported, } } }, capability_handlers = { - [LockCodes.ID] = { - [LockCodes.commands.updateCodes.NAME] = update_codes, - [LockCodes.commands.deleteCode.NAME] = delete_code, - [LockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, - [LockCodes.commands.requestCode.NAME] = request_code, - [LockCodes.commands.setCode.NAME] = set_code, - [LockCodes.commands.nameSlot.NAME] = name_slot, + [capabilities.lock.ID] = { + [capabilities.lock.commands.lock.NAME] = capability_handlers.lock, + [capabilities.lock.commands.unlock.NAME] = capability_handlers.unlock, }, - [Lock.ID] = { - [Lock.commands.lock.NAME] = lock, - [Lock.commands.unlock.NAME] = unlock, + [capabilities.lockUsers.ID] = { + [capabilities.lockUsers.commands.addUser.NAME] = capability_handlers.add_user, + [capabilities.lockUsers.commands.updateUser.NAME] = capability_handlers.update_user, + [capabilities.lockUsers.commands.deleteUser.NAME] = capability_handlers.delete_user, + [capabilities.lockUsers.commands.deleteAllUsers.NAME] = capability_handlers.delete_all_users, + }, + [capabilities.lockCredentials.ID] = { + [capabilities.lockCredentials.commands.addCredential.NAME] = capability_handlers.add_credential, + [capabilities.lockCredentials.commands.updateCredential.NAME] = capability_handlers.update_credential, + [capabilities.lockCredentials.commands.deleteCredential.NAME] = capability_handlers.delete_credential, + [capabilities.lockCredentials.commands.deleteAllCredentials.NAME] = capability_handlers.delete_all_credentials, }, [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = refresh - } + [capabilities.refresh.commands.refresh.NAME] = capability_handlers.refresh, + }, }, - sub_drivers = require("sub_drivers"), - lifecycle_handlers = { - doConfigure = do_configure, - added = device_added, - init = init, + supported_capabilities = { + capabilities.lock, + capabilities.lockCredentials, + capabilities.lockUsers, + capabilities.battery, }, + sub_drivers = require("sub_drivers"), health_check = false, shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_lock_driver, zigbee_lock_driver.supported_capabilities) -local lock = ZigbeeDriver("zigbee-lock", zigbee_lock_driver) -lock:run() +local driver = ZigbeeDriver("zigbee-lock", zigbee_lock_driver) +driver:run() diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua new file mode 100644 index 0000000000..81ff8f0e8a --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/can_handle.lua @@ -0,0 +1,12 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, ...) + local consts = require "lock_utils.constants" + local slga_migrated = device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) or false + if not slga_migrated then + local subdriver = require("legacy-handlers") + return true, subdriver + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua new file mode 100644 index 0000000000..b9d5ce78ad --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/init.lua @@ -0,0 +1,495 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Zigbee Driver utilities +local device_management = require "st.zigbee.device_management" + +-- Zigbee Spec Utils +local clusters = require "st.zigbee.zcl.clusters" +local Alarm = clusters.Alarms +local LockCluster = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration + +-- Capabilities +local capabilities = require "st.capabilities" +local Battery = capabilities.battery +local Lock = capabilities.lock +local LockCodes = capabilities.lockCodes + +-- Enums +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType +local ProgrammingEventCodeEnum = LockCluster.types.ProgramEventCode + +local socket = require "cosock.socket" +local lock_utils = require "legacy-handlers.legacy_lock_utils" + +local DELAY_LOCK_EVENT = "_delay_lock_event" +local MAX_DELAY = 10 + +local reload_all_codes = function(driver, device, command) + -- starts at first user code index then iterates through all lock codes as they come in + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) == nil) then + device:send(LockCluster.attributes.MaxPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) == nil) then + device:send(LockCluster.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) == nil) then + device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) + end + if (device:get_field(lock_utils.CHECKING_CODE) == nil) then device:set_field(lock_utils.CHECKING_CODE, 0) end + device:emit_event(LockCodes.scanCodes("Scanning", { visibility = { displayed = false } })) + device:send(LockCluster.server.commands.GetPINCode(device, device:get_field(lock_utils.CHECKING_CODE))) +end + +local refresh = function(driver, device, cmd) + device:refresh() + device:send(LockCluster.attributes.LockState:read(device)) + device:send(Alarm.attributes.AlarmCount:read(device)) + -- we can't determine from fingerprints if devices support lock codes, so + -- here in the driver we'll do a check once to see if the device responds here + -- and if it does, we'll switch it to a profile with lock codes + if not device:supports_capability_by_id(LockCodes.ID) and not device:get_field(lock_utils.CHECKED_CODE_SUPPORT) then + device:send(LockCluster.attributes.NumberOfPINUsersSupported:read(device)) + -- we won't make this value persist because it's not that important + device:set_field(lock_utils.CHECKED_CODE_SUPPORT, true) + end +end + +local do_configure = function(self, device) + device:send(device_management.build_bind_request(device, PowerConfiguration.ID, self.environment_info.hub_zigbee_eui)) + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1)) + + device:send(device_management.build_bind_request(device, LockCluster.ID, self.environment_info.hub_zigbee_eui)) + device:send(LockCluster.attributes.LockState:configure_reporting(device, 0, 3600, 0)) + + device:send(device_management.build_bind_request(device, Alarm.ID, self.environment_info.hub_zigbee_eui)) + device:send(Alarm.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0)) + + -- Don't send a reload all codes if this is a part of migration + if device.data.lockCodes == nil or device:get_field(lock_utils.MIGRATION_RELOAD_SKIPPED) == true then + device.thread:call_with_delay(2, function(d) + self:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + end) + else + device:set_field(lock_utils.MIGRATION_RELOAD_SKIPPED, true, { persist = true }) + end +end + +local alarm_handler = function(driver, device, zb_mess) + local ALARM_REPORT = { + [0] = Lock.lock.unknown(), + [1] = Lock.lock.unknown(), + -- Events 16-19 are low battery events, but are presented as descriptionText only + } + if (ALARM_REPORT[zb_mess.body.zcl_body.alarm_code.value] ~= nil) then + device:emit_event(ALARM_REPORT[zb_mess.body.zcl_body.alarm_code.value]) + end +end + +local get_pin_response_handler = function(driver, device, zb_mess) + local event = LockCodes.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + event.data = {codeName = lock_utils.get_code_name(device, code_slot)} + if (zb_mess.body.zcl_body.user_status.value == UserStatusEnum.OCCUPIED_ENABLED) then + -- Code slot is occupied + event.value = code_slot .. lock_utils.get_change_type(device, code_slot) + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = event.data.codeName + device:emit_event(event) + lock_utils.lock_codes_event(device, lock_codes) + lock_utils.reset_code_state(device, code_slot) + else + -- Code slot is unoccupied + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + -- Code has been deleted + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + else + -- Code is unset + event.value = code_slot .. " unset" + device:emit_event(event) + end + end + + code_slot = tonumber(code_slot) + if (code_slot == device:get_field(lock_utils.CHECKING_CODE)) then + -- the code we're checking has arrived + local last_slot = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) - 1 + if (code_slot >= last_slot) then + device:emit_event(LockCodes.scanCodes("Complete", { visibility = { displayed = false } })) + device:set_field(lock_utils.CHECKING_CODE, nil) + else + local checkingCode = device:get_field(lock_utils.CHECKING_CODE) + 1 + device:set_field(lock_utils.CHECKING_CODE, checkingCode) + device:send(LockCluster.server.commands.GetPINCode(device, checkingCode)) + end + end +end + +local programming_event_handler = function(driver, device, zb_mess) + local event = LockCodes.codeChanged("", { state_change = true }) + local code_slot = tostring(zb_mess.body.zcl_body.user_id.value) + event.data = {} + if (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.MASTER_CODE_CHANGED) then + -- Master code changed + event.value = "0 set" + event.data = {codeName = "Master Code"} + device:emit_event(event) + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_DELETED) then + if (zb_mess.body.zcl_body.user_id.value == 0xFF) then + -- All codes deleted + for cs, _ in pairs(lock_utils.get_lock_codes(device)) do + lock_utils.code_deleted(device, cs) + end + lock_utils.lock_codes_event(device, {}) + else + -- One code deleted + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + end + end + elseif (zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_ADDED or + zb_mess.body.zcl_body.program_event_code.value == ProgrammingEventCodeEnum.PIN_CODE_CHANGED) then + -- Code added or changed + local change_type = lock_utils.get_change_type(device, code_slot) + local code_name = lock_utils.get_code_name(device, code_slot) + event.value = code_slot .. change_type + event.data = {codeName = code_name} + device:emit_event(event) + if (change_type == " set") then + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = code_name + lock_utils.lock_codes_event(device, lock_codes) + end + end +end + +local handle_max_codes = function(driver, device, value) + if value.value ~= 0 then + -- Here's where we'll end up if we queried a lock whose profile does not have lock codes, + -- but it gave us a non-zero number of pin users, so we want to switch the profile + if not device:supports_capability_by_id(LockCodes.ID) then + device:try_update_metadata({ profile = "base-lock" }) -- switch to a lock with codes + lock_utils.populate_state_from_data(device) -- if this was a migrated device, try to migrate the lock codes + if not device:get_field(lock_utils.MIGRATION_COMPLETE) then -- this means we didn't find any pre-migration lock codes + -- so we'll load them manually + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + end + end + device:emit_event(LockCodes.maxCodes(value.value, { visibility = { displayed = false } })) + end +end + +local handle_max_code_length = function(driver, device, value) + device:emit_event(LockCodes.maxCodeLength(value.value, { visibility = { displayed = false } })) +end + +local handle_min_code_length = function(driver, device, value) + device:emit_event(LockCodes.minCodeLength(value.value, { visibility = { displayed = false } })) +end + +local update_codes = function(driver, device, command) + local delay = 0 + -- args.codes is json + for name, code in pairs(command.args.codes) do + -- these seem to come in the format "code[slot#]: code" + local code_slot = tonumber(string.gsub(name, "code", ""), 10) + if (code_slot ~= nil) then + if (code ~= nil and (code ~= "0" and code ~= "")) then + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.SetPINCode(device, + code_slot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + code)) + end) + delay = delay + 2 + else + device.thread:call_with_delay(delay, function() + device:send(LockCluster.server.commands.ClearPINCode(device, code_slot)) + end) + delay = delay + 2 + end + device.thread:call_with_delay(delay, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, code_slot)) + end) + delay = delay + 2 + end + end +end + +local delete_code = function(driver, device, command) + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, command.args.codeSlot)) + device.thread:call_with_delay(2, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) + end) +end + +local request_code = function(driver, device, command) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) +end + +local set_code = function(driver, device, command) + if (command.args.codePIN == "") then + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = { command.args.codeSlot, command.args.codeName } + }) + else + device:send(LockCluster.server.commands.SetPINCode(device, + command.args.codeSlot, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + command.args.codePIN) + ) + if (command.args.codeName ~= nil) then + -- wait for confirmation from the lock to commit this to memory + -- Groovy driver has a lot more info passed here as a description string, may need to be investigated + local codeState = device:get_field(lock_utils.CODE_STATE) or {} + codeState["setName" .. command.args.codeSlot] = command.args.codeName + device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) + end + + device.thread:call_with_delay(4, function(d) + device:send(LockCluster.server.commands.GetPINCode(device, command.args.codeSlot)) + end) + end +end + +local name_slot = function(driver, device, command) + local code_slot = tostring(command.args.codeSlot) + local lock_codes = lock_utils.get_lock_codes(device) + if (lock_codes[code_slot] ~= nil) then + lock_codes[code_slot] = command.args.codeName + device:emit_event(LockCodes.codeChanged(code_slot .. " renamed", { state_change = true })) + lock_utils.lock_codes_event(device, lock_codes) + end +end + +local function init(driver, device) + lock_utils.populate_state_from_data(device) + -- temp fix before this can be changed to non-persistent + device:set_field(lock_utils.CODE_STATE, nil, { persist = true }) +end + +-- The following two functions are from the lock defaults. They are in the base driver temporarily +-- until the fix is widely released in the lua libs +local lock_state_handler = function(driver, device, value, zb_rx) + local attr = capabilities.lock.lock + local LOCK_STATE = { + [value.NOT_FULLY_LOCKED] = attr.unknown(), + [value.LOCKED] = attr.locked(), + [value.UNLOCKED] = attr.unlocked(), + [value.UNDEFINED] = attr.unknown(), + } + + -- this is where we decide whether or not we need to delay our lock event because we've + -- observed it coming before the event (or we're starting to compute the timer) + local delay = device:get_field(DELAY_LOCK_EVENT) or 100 + if (delay < MAX_DELAY) then + device.thread:call_with_delay(delay+.5, function () + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, LOCK_STATE[value.value] or attr.unknown()) + end) + else + device:set_field(DELAY_LOCK_EVENT, socket.gettime()) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, LOCK_STATE[value.value] or attr.unknown()) + end +end + +local lock_operation_event_handler = function(driver, device, zb_rx) + local event_code = zb_rx.body.zcl_body.operation_event_code.value + local source = zb_rx.body.zcl_body.operation_event_source.value + local OperationEventCode = require "st.zigbee.generated.zcl_clusters.DoorLock.types.OperationEventCode" + local METHOD = { + [0] = "keypad", + [1] = "command", + [2] = "manual", + [3] = "rfid", + [4] = "fingerprint", + [5] = "bluetooth" + } + local STATUS = { + [OperationEventCode.LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), + [OperationEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), + [OperationEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() + } + local event = STATUS[event_code] + if (event ~= nil) then + event["data"] = {} + if (source ~= 0 and event_code == OperationEventCode.AUTO_LOCK or + event_code == OperationEventCode.SCHEDULE_LOCK or + event_code == OperationEventCode.SCHEDULE_UNLOCK + ) then + event.data.method = "auto" + else + event.data.method = METHOD[source] + end + if (source == 0 and device:supports_capability_by_id(capabilities.lockCodes.ID)) then --keypad + local code_id = zb_rx.body.zcl_body.user_id.value + local code_name = "Code " .. code_id + local lock_codes = device:get_field("lockCodes") + if (lock_codes ~= nil and + lock_codes[code_id] ~= nil) then + code_name = lock_codes[code_id] + end + event.data = { method = METHOD[0], codeId = code_id .. "", codeName = code_name } + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + if device:get_latest_state( + device:get_component_id_for_endpoint(zb_rx.address_header.src_endpoint.value), + capabilities.lock.ID, + capabilities.lock.lock.ID) == event.value.value then + local preceding_event_time = device:get_field(DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < MAX_DELAY then + device:set_field(DELAY_LOCK_EVENT, time_diff) + end + end + + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, event) + end +end + +local function lock(driver, device, command) + device:send_to_component(command.component, LockCluster.server.commands.LockDoor(device)) +end + +local function unlock(driver, device, command) + device:send_to_component(command.component, LockCluster.server.commands.UnlockDoor(device)) +end + +local migrate = function(driver, device, command) + local post_migration_consts = require "lock_utils.constants" + local LockUsers = capabilities.lockUsers + local LockCredentials = capabilities.lockCredentials + + -- set supported credentials + device:emit_event(LockCredentials.supportedCredentials({ post_migration_consts.CRED_TYPE_PIN }, { visibility = { displayed = false } })) + + -- migrate max/min credential length + local cached_min_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.minCodeLength.NAME) + local cached_max_code_len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) + if cached_min_code_len then + device:emit_event(LockCredentials.minPinCodeLen(cached_min_code_len, { visibility = { displayed = false } })) + end + if cached_max_code_len then + device:emit_event(LockCredentials.maxPinCodeLen(cached_max_code_len, { visibility = { displayed = false } })) + end + + -- migrate total codes supported + local cached_max_codes = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodes.NAME) + if cached_max_codes then + device:emit_event(LockCredentials.pinUsersSupported(cached_max_codes, { visibility = { displayed = false } })) + device:emit_event(LockUsers.totalUsersSupported(cached_max_codes, { visibility = { displayed = false } })) + end + + -- migrate stored lock codes slots and user names + local users, credentials = {}, {} + local cached_lock_codes = lock_utils.get_lock_codes(device) + local ordered_code_slots = {} + for code_slot_str, _ in pairs(cached_lock_codes) do + table.insert(ordered_code_slots, tonumber(code_slot_str)) + end + table.sort(ordered_code_slots) + for _, code_slot in ipairs(ordered_code_slots) do + local code_name = cached_lock_codes[tostring(code_slot)] + -- cached_lock_codes is formatted as { ["1"] = "Guest 1", ["2"] = "Guest 2" }, etc + if code_slot then + table.insert(users, { userIndex = code_slot, userType = "guest", userName = code_name }) + table.insert(credentials, { userIndex = code_slot, credentialIndex = code_slot, credentialType = post_migration_consts.CRED_TYPE_PIN, credentialName = code_name }) + end + end + -- manually ensure user/cred state is persisted, then emit events to populate the new capabilities with the migrated data. + device:set_field("persistedUsers", users, { persist = true }) + device:set_field("persistedCredentials", credentials, { persist = true }) + device:emit_event(LockUsers.users(users, { visibility = { displayed = false } })) + device:emit_event(LockCredentials.credentials(credentials, { visibility = { displayed = false } })) + + -- set and persist the migration complete flag and emit migrated event. Legacy subdriver will not be used again after this. + device:emit_event(LockCodes.migrated(true, { visibility = { displayed = false } })) + device:set_field(post_migration_consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + + -- try to refresh all unstored state + driver:inject_capability_command(device, { + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} + }) +end + +local legacy_capabilities_driver = { + NAME = "Lock Driver Using LockCodes Capability", + supported_capabilities = { + Lock, + LockCodes, + Battery, + }, + zigbee_handlers = { + cluster = { + [Alarm.ID] = { + [Alarm.client.commands.Alarm.ID] = alarm_handler + }, + [LockCluster.ID] = { + [LockCluster.client.commands.GetPINCodeResponse.ID] = get_pin_response_handler, + [LockCluster.client.commands.ProgrammingEventNotification.ID] = programming_event_handler, + [LockCluster.client.commands.OperatingEventNotification.ID] = lock_operation_event_handler + } + }, + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.LockState.ID] = lock_state_handler, + [LockCluster.attributes.MaxPINCodeLength.ID] = handle_max_code_length, + [LockCluster.attributes.MinPINCodeLength.ID] = handle_min_code_length, + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes, + } + } + }, + capability_handlers = { + [LockCodes.ID] = { + [LockCodes.commands.updateCodes.NAME] = update_codes, + [LockCodes.commands.deleteCode.NAME] = delete_code, + [LockCodes.commands.reloadAllCodes.NAME] = reload_all_codes, + [LockCodes.commands.requestCode.NAME] = request_code, + [LockCodes.commands.setCode.NAME] = set_code, + [LockCodes.commands.nameSlot.NAME] = name_slot, + [LockCodes.commands.migrate.NAME] = migrate, + }, + [Lock.ID] = { + [Lock.commands.lock.NAME] = lock, + [Lock.commands.unlock.NAME] = unlock, + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh + } + }, + sub_drivers = require("legacy-handlers.sub_drivers"), + lifecycle_handlers = { + doConfigure = do_configure, + init = init, + }, + health_check = false, + can_handle = require("legacy-handlers.can_handle") +} + +return legacy_capabilities_driver diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/legacy_lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/legacy_lock_utils.lua new file mode 100644 index 0000000000..6afa0aea3a --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/legacy_lock_utils.lua @@ -0,0 +1,89 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local utils = require "st.utils" +local capabilities = require "st.capabilities" +local json = require "st.json" +local LockCodes = capabilities.lockCodes + + +local legacy_lock_utils = { + -- Constants + LOCK_CODES = "lockCodes", + LOCK_USERS = "lockUsers", + CHECKING_CODE = "checkingCode", + CODE_STATE = "codeState", + MIGRATION_COMPLETE = "migrationComplete", + MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped", + CHECKED_CODE_SUPPORT = "checkedCodeSupport", +} + +legacy_lock_utils.get_lock_codes = function(device) + local lc = device:get_field(legacy_lock_utils.LOCK_CODES) + return lc ~= nil and lc or {} +end + +legacy_lock_utils.lock_codes_event = function(device, lock_codes) + device:set_field(legacy_lock_utils.LOCK_CODES, lock_codes, { persist = true } ) + device:emit_event(capabilities.lockCodes.lockCodes(json.encode(utils.deep_copy(lock_codes)), { visibility = { displayed = false } })) +end + + +function legacy_lock_utils.get_code_name(device, code_id) + if (device:get_field(legacy_lock_utils.CODE_STATE) ~= nil and device:get_field(legacy_lock_utils.CODE_STATE)["setName"..code_id] ~= nil) then + -- this means a code set operation succeeded + return device:get_field(legacy_lock_utils.CODE_STATE)["setName"..code_id] + elseif (legacy_lock_utils.get_lock_codes(device)[code_id] ~= nil) then + return legacy_lock_utils.get_lock_codes(device)[code_id] + else + return "Code " .. code_id + end +end + +function legacy_lock_utils.get_change_type(device, code_id) + if (legacy_lock_utils.get_lock_codes(device)[code_id] == nil) then + return " set" + else + return " changed" + end +end + +function legacy_lock_utils.reset_code_state(device, code_slot) + local codeState = device:get_field(legacy_lock_utils.CODE_STATE) + if (codeState ~= nil) then + codeState["setName".. code_slot] = nil + codeState["setCode".. code_slot] = nil + device:set_field(legacy_lock_utils.CODE_STATE, codeState, { persist = true }) + end +end + +function legacy_lock_utils.code_deleted(device, code_slot) + local lock_codes = legacy_lock_utils.get_lock_codes(device) + local event = LockCodes.codeChanged(code_slot.." deleted", { state_change = true }) + event.data = {codeName = legacy_lock_utils.get_code_name(device, code_slot)} + lock_codes[code_slot] = nil + device:emit_event(event) + legacy_lock_utils.reset_code_state(device, code_slot) + return lock_codes +end + +function legacy_lock_utils.populate_state_from_data(device) + if device.data.lockCodes ~= nil and device:get_field(legacy_lock_utils.MIGRATION_COMPLETE) ~= true then + -- build the lockCodes table + local lockCodes = {} + local lc_data = json.decode(device.data.lockCodes) + for k, v in pairs(lc_data) do + lockCodes[k] = v + end + -- Populate the devices `lockCodes` field + device:set_field(legacy_lock_utils.LOCK_CODES, utils.deep_copy(lockCodes), { persist = true }) + -- Populate the devices state history cache + device.state_cache["main"] = device.state_cache["main"] or {} + device.state_cache["main"][capabilities.lockCodes.ID] = device.state_cache["main"][capabilities.lockCodes.ID] or {} + device.state_cache["main"][capabilities.lockCodes.ID][capabilities.lockCodes.lockCodes.NAME] = {value = json.encode(utils.deep_copy(lockCodes))} + + device:set_field(legacy_lock_utils.MIGRATION_COMPLETE, true, { persist = true }) + end +end + +return legacy_lock_utils diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/can_handle.lua new file mode 100644 index 0000000000..326df876df --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/can_handle.lua @@ -0,0 +1,13 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +return function(opts, driver, device, cmd) + local fingerprints = require("legacy-handlers.lock-without-codes.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("legacy-handlers.lock-without-codes") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/fingerprints.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/lock-without-codes/fingerprints.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/fingerprints.lua diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/init.lua similarity index 96% rename from drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/init.lua index e5c6de3408..4522580297 100644 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/lock-without-codes/init.lua @@ -73,7 +73,7 @@ local lock_without_codes = { } } }, - can_handle = require("lock-without-codes.can_handle"), + can_handle = require("legacy-handlers.lock-without-codes.can_handle") } return lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/sub_drivers.lua new file mode 100644 index 0000000000..8cbfd6701f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/sub_drivers.lua @@ -0,0 +1,10 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lazy_load_if_possible = require "lazy_load_subdriver" +local sub_drivers = { + lazy_load_if_possible("legacy-handlers.yale"), + lazy_load_if_possible("legacy-handlers.yale-fingerprint-lock"), + lazy_load_if_possible("legacy-handlers.lock-without-codes") +} +return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/can_handle.lua new file mode 100644 index 0000000000..3baa8f368c --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/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 fingerprints = require("yale-fingerprint-lock.fingerprints") + for _, fingerprint in ipairs(fingerprints) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("legacy-handlers.yale-fingerprint-lock") + return true, subdriver + end + end + return false +end diff --git a/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/init.lua new file mode 100644 index 0000000000..7e982fd943 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale-fingerprint-lock/init.lua @@ -0,0 +1,27 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local LockCluster = clusters.DoorLock +local LockCodes = capabilities.lockCodes + +local YALE_FINGERPRINT_MAX_CODES = 0x1E +local handle_max_codes = function(driver, device, value) + device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) +end + +local yale_fingerprint_lock_driver = { + NAME = "YALE Fingerprint Lock", + zigbee_handlers = { + attr = { + [LockCluster.ID] = { + [LockCluster.attributes.NumberOfPINUsersSupported.ID] = handle_max_codes + } + } + }, + can_handle = require("legacy-handlers.yale-fingerprint-lock.can_handle") +} + +return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/can_handle.lua similarity index 85% rename from drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/can_handle.lua index 54340c7811..832914f6ea 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/can_handle.lua @@ -3,7 +3,7 @@ local function yale_can_handle(opts, driver, device, ...) if device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" then - return true, require("yale") + return true, require("legacy-handlers.yale") end return false end diff --git a/drivers/SmartThings/zigbee-lock/src/yale/init.lua b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/init.lua similarity index 97% rename from drivers/SmartThings/zigbee-lock/src/yale/init.lua rename to drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/init.lua index c315fbfa06..a85a9f8798 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/legacy-handlers/yale/init.lua @@ -15,7 +15,7 @@ local LockCodes = capabilities.lockCodes local UserStatusEnum = LockCluster.types.DrlkUserStatus local UserTypeEnum = LockCluster.types.DrlkUserType -local lock_utils = (require "lock_utils") +local lock_utils = (require "legacy-handlers.legacy_lock_utils") local reload_all_codes = function(driver, device, command) -- starts at first user code index then iterates through all lock codes as they come in @@ -142,8 +142,7 @@ local yale_door_lock_driver = { [LockCodes.commands.setCode.NAME] = set_code } }, - sub_drivers = require("yale.sub_drivers"), - can_handle = require("yale.can_handle"), + can_handle = require("legacy-handlers.yale.can_handle") } return yale_door_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua deleted file mode 100644 index 543e43a8b1..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/lock-without-codes/can_handle.lua +++ /dev/null @@ -1,14 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function can_handle_lock_without_codes(opts, driver, device) - local FINGERPRINTS = require("lock-without-codes.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("lock-without-codes") - end - end - return false -end - -return can_handle_lock_without_codes diff --git a/drivers/SmartThings/zigbee-lock/src/lock_handlers/capabilities.lua b/drivers/SmartThings/zigbee-lock/src/lock_handlers/capabilities.lua new file mode 100644 index 0000000000..371cf7a765 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock_handlers/capabilities.lua @@ -0,0 +1,248 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local lock_utils = require "lock_utils.utils" +local tables = require "lock_utils.tables" +local consts = require "lock_utils.constants" + +local Alarm = clusters.Alarms +local LockCluster = clusters.DoorLock +local UserStatusEnum = LockCluster.types.DrlkUserStatus +local UserTypeEnum = LockCluster.types.DrlkUserType + + +local CapabilityHandlers = {} + + +-- [[ LOCK CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.lock(driver, device, command) + device:send_to_component(command.component, LockCluster.server.commands.LockDoor(device)) +end + +function CapabilityHandlers.unlock(driver, device, command) + device:send_to_component(command.component, LockCluster.server.commands.UnlockDoor(device)) +end + + +-- [[ LOCK USERS CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.add_user(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.ADD, consts.COMMAND_RESULT.BUSY) + return + end + + -- Find the smallest positive userIndex not already in the table + local next_available_index = tables.next_index(device, "users") + local status = tables.add_entry(device, "users", { + userIndex = next_available_index, + userName = command.args.userName, + userType = command.args.userType, + }) + local additional_info = status == consts.COMMAND_RESULT.SUCCESS and { userIndex = next_available_index } + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.ADD, status, additional_info) +end + +function CapabilityHandlers.update_user(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.UPDATE, consts.COMMAND_RESULT.BUSY) + return + end + + local result_status + if not tables.find_entry(device, "users", command.args.userIndex) then + result_status = tables.add_entry(device, "users", { + userIndex = command.args.userIndex, + userName = command.args.userName, + userType = command.args.userType, + }) + else + result_status = tables.update_entry(device, "users", + command.args.userIndex, + { userName = command.args.userName, userType = command.args.userType } + ) + end + + local additional_info = result_status == consts.COMMAND_RESULT.SUCCESS and { userIndex = command.args.userIndex } + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.UPDATE, result_status, additional_info) +end + +function CapabilityHandlers.delete_user(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE, consts.COMMAND_RESULT.BUSY) + return + end + + local associated_credentials = tables.find_all_entries_by(device, "credentials", "userIndex", command.args.userIndex) + for _, associated_credential in ipairs(associated_credentials) do + -- Set busy state with the full user+credential context BEFORE injecting. + -- Injected capability commands are schema-validated, so extra args like userIndex + -- would be stripped. By setting device fields here we preserve the full context. + lock_utils.set_busy_state(device, consts.LOCK_USERS.DELETE, { + userIndex = command.args.userIndex, + credentialIndex = associated_credential.credentialIndex, + credentialType = consts.CRED_TYPE_PIN, + }) + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteCredential.NAME, + args = { + credentialIndex = associated_credential.credentialIndex, + credentialType = consts.CRED_TYPE_PIN, + } + }) + end + if #associated_credentials == 0 then + -- No associated credentials: delete the user entry directly and report the result + local status = tables.delete_entry(device, "users", command.args.userIndex) + local additional_info = status == consts.COMMAND_RESULT.SUCCESS and { userIndex = command.args.userIndex } + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE, status, additional_info) + end +end + +function CapabilityHandlers.delete_all_users(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE_ALL, consts.COMMAND_RESULT.BUSY) + return + end + -- Set busy state with DELETE_ALL context BEFORE injecting so the response handler + -- knows to clear both tables and emit results for both capabilities. + lock_utils.set_busy_state(device, consts.LOCK_USERS.DELETE_ALL, {}) + driver:inject_capability_command(device, { + capability = capabilities.lockCredentials.ID, + command = capabilities.lockCredentials.commands.deleteAllCredentials.NAME, + args = { credentialType = consts.CRED_TYPE_PIN } + }) +end + + +-- [[ LOCK CREDENTIALS CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.add_credential(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.BUSY) + return + end + + -- A userIndex of 0 means "auto-assign the next available slot" + local user_index = command.args.userIndex == 0 and tables.next_index(device, "users") or command.args.userIndex + local cred_index = command.args.userIndex == 0 and tables.next_index(device, "credentials") or command.args.userIndex + + if #(tables.get_state(device, "credentials") or {}) >= tables.get_max_entries(device, "credentials") then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.ADD, consts.COMMAND_RESULT.RESOURCE_EXHAUSTED) + return + end + + -- Set busy state and attempt to set the PIN on-device. + lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.ADD, { + userIndex = user_index, + credentialIndex = cred_index, + credentialType = command.args.credentialType, + credentialName = command.args.credentialName, + }) + device:send(LockCluster.server.commands.SetPINCode(device, + cred_index, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + command.args.credentialData) + ) +end + +function CapabilityHandlers.update_credential(driver, device, command) + if lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.BUSY) + return + end + + if not tables.find_entry(device, "credentials", command.args.credentialIndex) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.UPDATE, consts.COMMAND_RESULT.FAILURE) + return + end + + lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.UPDATE, { + userIndex = command.args.userIndex, + credentialIndex = command.args.credentialIndex, + credentialType = command.args.credentialType, + credentialName = command.args.credentialName, + }) + device:send(LockCluster.server.commands.SetPINCode(device, + command.args.credentialIndex, + UserStatusEnum.OCCUPIED_ENABLED, + UserTypeEnum.UNRESTRICTED, + command.args.credentialData) + ) +end + +function CapabilityHandlers.delete_credential(driver, device, command) + local cmd_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + + if cmd_in_progress == consts.LOCK_USERS.DELETE then + -- Injected by deleteUser; busy state was already set with the full LOCK_USERS.DELETE context. + local credential_args = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) or {} + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, credential_args.credentialIndex)) + elseif lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, consts.COMMAND_RESULT.BUSY) + else + -- Standalone deleteCredential: look up the credential to obtain its associated userIndex. + local found_cred = tables.find_entry(device, "credentials", command.args.credentialIndex) + if not found_cred then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, consts.COMMAND_RESULT.FAILURE) + return + end + lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.DELETE, { + credentialIndex = command.args.credentialIndex, + credentialType = command.args.credentialType, + userIndex = found_cred.userIndex, + }) + device:send(LockCluster.attributes.SendPINOverTheAir:write(device, true)) + device:send(LockCluster.server.commands.ClearPINCode(device, command.args.credentialIndex)) + end +end + +function CapabilityHandlers.delete_all_credentials(driver, device, command) + local cmd_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + + if cmd_in_progress == consts.LOCK_USERS.DELETE_ALL then + -- Injected by deleteAllUsers; busy state was already set with LOCK_USERS.DELETE_ALL context. + device:send(LockCluster.server.commands.ClearAllPINCodes(device)) + elseif lock_utils.is_device_busy(device) then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE_ALL, consts.COMMAND_RESULT.BUSY) + else + lock_utils.set_busy_state(device, consts.LOCK_CREDENTIALS.DELETE_ALL, command.args) + device:send(LockCluster.server.commands.ClearAllPINCodes(device)) + end +end + + +-- [[ REFRESH CAPABILITY COMMANDS ]] -- + +function CapabilityHandlers.refresh(driver, device, cmd) + device:refresh() + device:send(LockCluster.attributes.LockState:read(device)) + device:send(Alarm.attributes.AlarmCount:read(device)) + + if device:supports_capability(capabilities.lockCredentials) then + -- If we are missing the cached values for these attributes, read them so we can properly manage them locally + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) == nil) then + device:send(clusters.DoorLock.attributes.MaxPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.minPinCodeLen.NAME) == nil) then + device:send(clusters.DoorLock.attributes.MinPINCodeLength:read(device)) + end + if (device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.pinUsersSupported.NAME) == nil) or + (device:get_latest_state("main", capabilities.lockUsers.ID, capabilities.lockUsers.totalUsersSupported.NAME) == nil) then + device:send(clusters.DoorLock.attributes.NumberOfPINUsersSupported:read(device)) + end + elseif not device:supports_capability(capabilities.lockCodes) then + -- Generically fingerprinted devices may support PINs even though the profile does not yet + -- reflect it. Read NumberOfPINUsersSupported so the response handler can trigger a profile + -- migration when appropriate. + device:send(clusters.DoorLock.attributes.NumberOfPINUsersSupported:read(device)) + end +end + +return CapabilityHandlers diff --git a/drivers/SmartThings/zigbee-lock/src/lock_handlers/zigbee_responses.lua b/drivers/SmartThings/zigbee-lock/src/lock_handlers/zigbee_responses.lua new file mode 100644 index 0000000000..9d1c53947a --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock_handlers/zigbee_responses.lua @@ -0,0 +1,438 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local socket = require "cosock.socket" + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local consts = require "lock_utils.constants" +local lock_utils = require "lock_utils.utils" +local tables = require "lock_utils.tables" + +local ZigbeeHandlers = {} + + +-- [[ DOOR LOCK CLUSTER COMMAND RESPONSES ]] -- + +function ZigbeeHandlers.set_pin_code_response(driver, device, zb_rx) + -- cached values from capability command + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + local credential_args_in_use = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) + -- zb response values + local set_pin_code_status = zb_rx.body.zcl_body.status.value + + local SetCodeStatus = clusters.DoorLock.types.DrlkSetCodeStatus + -- SUCCESS = 0 + -- GENERAL_FAILURE = 1 + -- MEMORY_FULL = 2 + -- DUPLICATE_CODE = 3 + + -- mapped failures states + local RESPONSE_RESULT_MAP = { + [SetCodeStatus.GENERAL_FAILURE] = consts.COMMAND_RESULT.FAILURE, + [SetCodeStatus.MEMORY_FULL] = consts.COMMAND_RESULT.RESOURCE_EXHAUSTED, + [SetCodeStatus.DUPLICATE_CODE] = consts.COMMAND_RESULT.DUPLICATE, + } + + -- apply result based on response and identify command result status + local result_status + if set_pin_code_status == SetCodeStatus.SUCCESS then + if command_in_progress == consts.LOCK_CREDENTIALS.ADD then + tables.add_entry(device, "users", { + userIndex = credential_args_in_use.userIndex, + userName = "Guest " .. credential_args_in_use.userIndex, -- default + userType = "guest", -- default + }) + result_status = tables.add_entry(device, "credentials", { + userIndex = credential_args_in_use.userIndex, + credentialIndex = credential_args_in_use.credentialIndex, + credentialType = credential_args_in_use.credentialType, + credentialName = "Guest " .. credential_args_in_use.userIndex, -- default + }) + elseif command_in_progress == consts.LOCK_CREDENTIALS.UPDATE then + result_status = consts.COMMAND_RESULT.SUCCESS + end + elseif RESPONSE_RESULT_MAP[set_pin_code_status] then + result_status = RESPONSE_RESULT_MAP[set_pin_code_status] + else + result_status = consts.COMMAND_RESULT.FAILURE + end + + -- emit command result + if command_in_progress then + local additional_info = result_status == consts.COMMAND_RESULT.SUCCESS and { + userIndex = credential_args_in_use.userIndex, + credentialIndex = credential_args_in_use.credentialIndex, + } or nil + lock_utils.emit_command_result(device, capabilities.lockCredentials, command_in_progress, result_status, additional_info) + lock_utils.clear_busy_state(device) + end +end + + +function ZigbeeHandlers.clear_all_pin_codes_response(driver, device, zb_rx) + -- cached values from capability command + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + -- zb response values + local clear_pin_code_status = zb_rx.body.zcl_body.status.value + + local ResponseStatus = clusters.DoorLock.types.DrlkPassFailStatus + -- PASS = 0 + -- FAIL = 1 + + -- apply result and identify command result statuses + local user_status, credential_status + if clear_pin_code_status == ResponseStatus.PASS then + -- Only clear the users table when this response is for a deleteAllUsers flow. + if command_in_progress == consts.LOCK_USERS.DELETE_ALL then + user_status = tables.delete_all_entries(device, "users") + end + credential_status = tables.delete_all_entries(device, "credentials") + elseif clear_pin_code_status == ResponseStatus.FAIL then + user_status = consts.COMMAND_RESULT.FAILURE + credential_status = consts.COMMAND_RESULT.FAILURE + end + + -- emit command results + if command_in_progress == consts.LOCK_USERS.DELETE_ALL then + -- deleteAllUsers injects deleteAllCredentials, so both command results should be emitted. + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE_ALL, user_status) + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE_ALL, credential_status) + elseif command_in_progress == consts.LOCK_CREDENTIALS.DELETE_ALL then + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE_ALL, credential_status) + end + lock_utils.clear_busy_state(device) +end + + +function ZigbeeHandlers.clear_pin_code_response(driver, device, zb_rx) + -- cached values from capability command + local credential_args_in_use = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + -- guard: avoid nil errors if the programming event failsafe already handled this command and cleared the busy state + if not credential_args_in_use then return end + + -- zb response values + local clear_pin_code_status = zb_rx.body.zcl_body.status.value + + local ResponseStatus = clusters.DoorLock.types.DrlkPassFailStatus + -- PASS = 0 + -- FAIL = 1 + + -- apply result and identify command result statuses + local user_status, credential_status + if clear_pin_code_status == ResponseStatus.PASS then + credential_status = tables.delete_entry(device, "credentials", credential_args_in_use.credentialIndex) + if command_in_progress == consts.LOCK_USERS.DELETE or tables.find_entry_by(device, "credentials", "userIndex", credential_args_in_use.userIndex) == nil then + user_status = tables.delete_entry(device, "users", credential_args_in_use.userIndex) + end + elseif clear_pin_code_status == ResponseStatus.FAIL then + user_status = consts.COMMAND_RESULT.FAILURE + credential_status = consts.COMMAND_RESULT.FAILURE + end + + -- emit command results + if command_in_progress == consts.LOCK_USERS.DELETE then + -- the deleteUser command injects a deleteCredential command, so both command results should be emitted in this case. + local user_info = user_status == consts.COMMAND_RESULT.SUCCESS and { userIndex = credential_args_in_use.userIndex } or nil + local cred_info = credential_status == consts.COMMAND_RESULT.SUCCESS and { credentialIndex = credential_args_in_use.credentialIndex, userIndex = credential_args_in_use.userIndex } or nil + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE, user_status, user_info) + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, credential_status, cred_info) + elseif command_in_progress == consts.LOCK_CREDENTIALS.DELETE then + local cred_info = credential_status == consts.COMMAND_RESULT.SUCCESS and { credentialIndex = credential_args_in_use.credentialIndex, userIndex = credential_args_in_use.userIndex } or nil + lock_utils.emit_command_result(device, capabilities.lockCredentials, consts.LOCK_CREDENTIALS.DELETE, credential_status, cred_info) + end + lock_utils.clear_busy_state(device) +end + + +function ZigbeeHandlers.get_pin_code_response(driver, device, zb_rx) + -- cached values from capability command + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + -- zb response values + local user_id = tonumber(zb_rx.body.zcl_body.user_id.value) + + if command_in_progress == consts.SYNC.CODES_FROM_LOCK then + -- Only add entries for slots that are actually occupied on the lock. + local user_status = zb_rx.body.zcl_body.user_status.value + if user_status == clusters.DoorLock.types.DrlkUserStatus.OCCUPIED_ENABLED then + -- If an entry already exists at this index, this will be a no-op. + tables.add_entry(device, "users", { + userIndex = user_id, + userName = "Guest " .. user_id, + userType = "guest", + }) + tables.add_entry(device, "credentials", { + userIndex = user_id, + credentialIndex = user_id, + credentialType = consts.CRED_TYPE_PIN, + credentialName = "Guest " .. user_id, + }) + -- reset the consecutive unoccupied codes counter since we found an occupied code + device:set_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES, 0) + else + local consecutive_unoccupied_codes = (device:get_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES) or 0) + 1 + device:set_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES, consecutive_unoccupied_codes) + end + + if user_id >= tables.get_max_entries(device, "credentials") or + (device:get_field(consts.SYNC.CONSECUTIVE_UNOCCUPIED_CODES) or 0) > 5 then + -- stop if we hit the max number of codes supported by the lock, or if we have received 5 consecutive unoccupied codes. + device:set_field(consts.SYNC.CODE_INDEX, nil) + lock_utils.clear_busy_state(device) + else + local synced_code_index = device:get_field(consts.SYNC.CODE_INDEX) + 1 + device:set_field(consts.SYNC.CODE_INDEX, synced_code_index) + lock_utils.set_busy_state(device, consts.SYNC.CODES_FROM_LOCK, { checkingCode = synced_code_index }) + device:send(clusters.DoorLock.server.commands.GetPINCode(device, synced_code_index)) + end + end +end + + +-- [[ DOOR LOCK CLUSTER EVENT NOTIFICATIONS ]] -- + +function ZigbeeHandlers.programming_event_notification(driver, device, zb_rx) + -- zb response values + local user_id = tonumber(zb_rx.body.zcl_body.user_id.value) + local event_code = tonumber(zb_rx.body.zcl_body.program_event_code.value) + + if device:get_manufacturer() == "ASSA ABLOY iRevo" or device:get_manufacturer() == "Yale" then + if user_id >= 256 then -- Index is incorrectly written on these devices. Attempt to shift it to get an actual value + user_id = user_id >> 8 + end + end + + local ProgramEventCode = clusters.DoorLock.types.ProgramEventCode + -- MASTER_CODE_CHANGED = 1 + -- PIN_CODE_ADDED = 2 + -- PIN_CODE_DELETED = 3 + -- PIN_CODE_CHANGED = 4 + -- RFID_CODE_ADDED = 5 + -- RFID_CODE_DELETED = 6 + + + -- failsafes: handle the case where we receive a programming event notification for a command we've just sent, + -- which can be verified by checking that the user id matches the one used in the command, but before we receive + -- the response for that command. This gives us double the chance to handle the command + -- in case the response handler doesn't execute properly for some reason. + -- + -- cached values from capability command, if applicable. + local command_in_progress = device:get_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS) + local credential_args_in_use = device:get_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE) or {} + if command_in_progress and credential_args_in_use.credentialIndex == user_id then + local result_status + if event_code == ProgramEventCode.PIN_CODE_ADDED then + if command_in_progress == consts.LOCK_CREDENTIALS.ADD then + tables.add_entry(device, "users", { + userIndex = credential_args_in_use.userIndex, + userName = "Guest " .. credential_args_in_use.userIndex, -- default + userType = "guest", -- default + }) + result_status = tables.add_entry(device, "credentials", { + userIndex = credential_args_in_use.userIndex, + credentialIndex = credential_args_in_use.credentialIndex, + credentialType = credential_args_in_use.credentialType, + credentialName = credential_args_in_use.credentialName, -- optional + }) + elseif command_in_progress == consts.LOCK_CREDENTIALS.UPDATE then + -- some devices emit PIN_CODE_ADDED even for UPDATE events + result_status = consts.COMMAND_RESULT.SUCCESS + end + elseif event_code == ProgramEventCode.PIN_CODE_DELETED and (command_in_progress == consts.LOCK_CREDENTIALS.DELETE or command_in_progress == consts.LOCK_USERS.DELETE) then + result_status = tables.delete_entry(device, "credentials", credential_args_in_use.credentialIndex) + -- the deleteUser command injects a deleteCredential command, so ensure the lockUsers command results is emitted in this case. + if command_in_progress == consts.LOCK_USERS.DELETE or tables.find_entry_by(device, "credentials", "userIndex", credential_args_in_use.userIndex) == nil then + local user_status = tables.delete_entry(device, "users", credential_args_in_use.userIndex) + local user_info = user_status == consts.COMMAND_RESULT.SUCCESS and { userIndex = credential_args_in_use.userIndex } or nil + if command_in_progress == consts.LOCK_USERS.DELETE then + lock_utils.emit_command_result(device, capabilities.lockUsers, consts.LOCK_USERS.DELETE, user_status, user_info) + command_in_progress = consts.LOCK_CREDENTIALS.DELETE -- ensure the lockCredentials result is emitted after this with the correct command context + end + end + elseif event_code == ProgramEventCode.PIN_CODE_CHANGED and command_in_progress == consts.LOCK_CREDENTIALS.UPDATE then + result_status = consts.COMMAND_RESULT.SUCCESS + end + + if result_status then + lock_utils.emit_command_result(device, + capabilities.lockCredentials, + command_in_progress, + result_status, + { userIndex = credential_args_in_use.userIndex, credentialIndex = credential_args_in_use.credentialIndex } + ) + lock_utils.clear_busy_state(device) + return + end + end + + -- handle the case where we receive a programming event notification for a code we've just deleted, + if event_code == ProgramEventCode.PIN_CODE_ADDED then + -- if no "addCredential" command is in progress, check if the credential is already stored locally. + local credential = tables.find_entry(device, "credentials", user_id) + if not credential then + -- check what user slot we have to associate this with and add a default user + local next_index = tables.next_index(device, "users") + if next_index and next_index <= tables.get_max_entries(device, "users") then + tables.add_entry(device, "users", { + userIndex = next_index, + userName = "Guest " .. next_index, + userType = "guest", + }) + tables.add_entry(device, "credentials", { + userIndex = next_index, + credentialIndex = user_id, + credentialType = consts.CRED_TYPE_PIN, + credentialName = "Guest " .. next_index, + }) + end + end + elseif event_code == ProgramEventCode.PIN_CODE_DELETED then + -- check if a credential exists locally for this user id, then + -- try to delete the entries in our tables corresponding to this code. + local credential = tables.find_entry(device, "credentials", user_id) + if credential then + tables.delete_entry(device, "credentials", user_id) + -- Only delete the user if they have no remaining credentials + if tables.find_entry_by(device, "credentials", "userIndex", credential.userIndex) == nil then + tables.delete_entry(device, "users", credential.userIndex) + end + end + end +end + +function ZigbeeHandlers.operating_event_notification(driver, device, zb_rx) + local op_event_code = tonumber(zb_rx.body.zcl_body.operation_event_code.value) + local op_event_source = tonumber(zb_rx.body.zcl_body.operation_event_source.value) + local user_id = tonumber(zb_rx.body.zcl_body.user_id.value) + + -- get lock event or return + local OpEventCode = clusters.DoorLock.types.OperationEventCode + local OP_EVENT_CODE_CAPABILITY_MAP = { + [OpEventCode.LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), + [OpEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), + [OpEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), + [OpEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() + } + local lock_event = OP_EVENT_CODE_CAPABILITY_MAP[op_event_code] + if not lock_event then return end + lock_event.data = {} + + -- get method of lock event + local OpEventSource = clusters.DoorLock.types.DrlkOperationEventSource + local OP_EVENT_SOURCE_CAPABILITY_MAP = { + [OpEventSource.KEYPAD] = "keypad", + [OpEventSource.RF] = "command", + [OpEventSource.MANUAL] = "manual", + [OpEventSource.RFID] = "rfid", + } + if op_event_code == OpEventCode.AUTO_LOCK or + op_event_code == OpEventCode.SCHEDULE_LOCK or + op_event_code == OpEventCode.SCHEDULE_UNLOCK + then + lock_event.data.method = "auto" + else + lock_event.data.method = OP_EVENT_SOURCE_CAPABILITY_MAP[op_event_source] or "manual" + end + + -- get stored lockUsers data if applicable + if op_event_source == OpEventSource.KEYPAD and device:supports_capability(capabilities.lockUsers) then + local credential = tables.find_entry(device, "credentials", user_id) + local associated_user = credential and tables.find_entry_by(device, "users", "userIndex", credential.userIndex) or nil + if associated_user then + lock_event.data.userIndex = associated_user.userIndex + lock_event.data.userName = associated_user.userName + lock_event.data.userType = associated_user.userType + else + lock_event.data.userIndex = user_id + lock_event.data.userName = "Guest " .. user_id -- default + end + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + local endpoint_id = zb_rx.address_header.src_endpoint.value + if lock_event.value.value == device:get_latest_state( + device:get_component_id_for_endpoint(endpoint_id), + capabilities.lock.ID, + capabilities.lock.lock.ID + ) then + local preceding_event_time = device:get_field(consts.DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < consts.MAX_DELAY then + device:set_field(consts.DELAY_LOCK_EVENT, time_diff) + end + end + + device:emit_event_for_endpoint(endpoint_id, lock_event) +end + + +-- [[ DOOR LOCK CLUSTER ATTRIBUTES ]] -- + +function ZigbeeHandlers.lock_state(driver, device, value, zb_rx) + local attr = capabilities.lock.lock + local LOCK_STATE = { + [value.NOT_FULLY_LOCKED] = attr.unknown(), + [value.LOCKED] = attr.locked(), + [value.UNLOCKED] = attr.unlocked(), + [value.UNDEFINED] = attr.unknown(), + } + + -- this is where we decide whether or not we need to delay our lock event because we've + -- observed it coming before the event (or we're starting to compute the timer) + local delay = device:get_field(consts.DELAY_LOCK_EVENT) or 100 + if (delay < consts.MAX_DELAY) then + device.thread:call_with_delay(delay+.5, function () + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, LOCK_STATE[value.value] or attr.unknown()) + end) + else + device:set_field(consts.DELAY_LOCK_EVENT, socket.gettime()) + device:emit_event_for_endpoint(zb_rx.address_header.src_endpoint.value, LOCK_STATE[value.value] or attr.unknown()) + end +end + +function ZigbeeHandlers.max_pin_code_length(driver, device, value) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(value.value, { visibility = { displayed = false } })) +end + +function ZigbeeHandlers.min_pin_code_length(driver, device, value) + device:emit_event(capabilities.lockCredentials.minPinCodeLen(value.value, { visibility = { displayed = false } })) +end + +function ZigbeeHandlers.number_of_pin_users_supported(driver, device, value) + if not device:supports_capability_by_id(capabilities.lockCodes.ID) and value.value > 0 then + -- this device was generically fingerprinted, but supports PIN users, so we should migrate it. + device:try_update_metadata({ profile = "base-lock" }) + end + if device:supports_capability(capabilities.lockCredentials) then + device:emit_event(capabilities.lockCredentials.pinUsersSupported(value.value, {visibility = {displayed = false}})) + end + if device:supports_capability(capabilities.lockUsers) then + device:emit_event(capabilities.lockUsers.totalUsersSupported(value.value, {visibility = {displayed = false}})) + end +end + + +-- [[ ALARMS CLUSTER COMMANDS ]] -- + +function ZigbeeHandlers.alarm(driver, device, zb_rx) + local ALARM_REPORT = { + [0] = capabilities.lock.lock.unknown(), + [1] = capabilities.lock.lock.unknown(), + -- Events 16-19 are low battery events, but are presented as descriptionText only + } + if (ALARM_REPORT[zb_rx.body.zcl_body.alarm_code.value] ~= nil) then + device:emit_event(ALARM_REPORT[zb_rx.body.zcl_body.alarm_code.value]) + end +end + + +return ZigbeeHandlers diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils.lua deleted file mode 100644 index a02a59963c..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/lock_utils.lua +++ /dev/null @@ -1,87 +0,0 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 -local utils = require "st.utils" -local capabilities = require "st.capabilities" -local json = require "st.json" -local LockCodes = capabilities.lockCodes - - -local lock_utils = { - -- Constants - LOCK_CODES = "lockCodes", - CHECKING_CODE = "checkingCode", - CODE_STATE = "codeState", - MIGRATION_COMPLETE = "migrationComplete", - MIGRATION_RELOAD_SKIPPED = "migrationReloadSkipped", - CHECKED_CODE_SUPPORT = "checkedCodeSupport" -} - -lock_utils.get_lock_codes = function(device) - local lc = device:get_field(lock_utils.LOCK_CODES) - return lc ~= nil and lc or {} -end - -lock_utils.lock_codes_event = function(device, lock_codes) - device:set_field(lock_utils.LOCK_CODES, lock_codes, { persist = true } ) - device:emit_event(capabilities.lockCodes.lockCodes(json.encode(utils.deep_copy(lock_codes)), { visibility = { displayed = false } })) -end - - -function lock_utils.get_code_name(device, code_id) - if (device:get_field(lock_utils.CODE_STATE) ~= nil and device:get_field(lock_utils.CODE_STATE)["setName"..code_id] ~= nil) then - -- this means a code set operation succeeded - return device:get_field(lock_utils.CODE_STATE)["setName"..code_id] - elseif (lock_utils.get_lock_codes(device)[code_id] ~= nil) then - return lock_utils.get_lock_codes(device)[code_id] - else - return "Code " .. code_id - end -end - -function lock_utils.get_change_type(device, code_id) - if (lock_utils.get_lock_codes(device)[code_id] == nil) then - return " set" - else - return " changed" - end -end - -function lock_utils.reset_code_state(device, code_slot) - local codeState = device:get_field(lock_utils.CODE_STATE) - if (codeState ~= nil) then - codeState["setName".. code_slot] = nil - codeState["setCode".. code_slot] = nil - device:set_field(lock_utils.CODE_STATE, codeState, { persist = true }) - end -end - -function lock_utils.code_deleted(device, code_slot) - local lock_codes = lock_utils.get_lock_codes(device) - local event = LockCodes.codeChanged(code_slot.." deleted", { state_change = true }) - event.data = {codeName = lock_utils.get_code_name(device, code_slot)} - lock_codes[code_slot] = nil - device:emit_event(event) - lock_utils.reset_code_state(device, code_slot) - return lock_codes -end - -function lock_utils.populate_state_from_data(device) - if device.data.lockCodes ~= nil and device:get_field(lock_utils.MIGRATION_COMPLETE) ~= true then - -- build the lockCodes table - local lockCodes = {} - local lc_data = json.decode(device.data.lockCodes) - for k, v in pairs(lc_data) do - lockCodes[k] = v - end - -- Populate the devices `lockCodes` field - device:set_field(lock_utils.LOCK_CODES, utils.deep_copy(lockCodes), { persist = true }) - -- Populate the devices state history cache - device.state_cache["main"] = device.state_cache["main"] or {} - device.state_cache["main"][capabilities.lockCodes.ID] = device.state_cache["main"][capabilities.lockCodes.ID] or {} - device.state_cache["main"][capabilities.lockCodes.ID][capabilities.lockCodes.lockCodes.NAME] = {value = json.encode(utils.deep_copy(lockCodes))} - - device:set_field(lock_utils.MIGRATION_COMPLETE, true, { persist = true }) - end -end - -return lock_utils diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils/constants.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils/constants.lua new file mode 100644 index 0000000000..78537182f1 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils/constants.lua @@ -0,0 +1,47 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local lock_constants = {} + +lock_constants.DRIVER_STATE = { + BUSY = "busy", + COMMAND_IN_PROGRESS = "commandInProgress", + CREDENTIAL_ARGS_IN_USE = "currentCredential", + SLGA_MIGRATED = "slgaMigrated", +} + +lock_constants.SYNC = { + CODES_FROM_LOCK = "syncCodesFromLock", + CODE_INDEX = "syncCodeIndex", + CONSECUTIVE_UNOCCUPIED_CODES = "consecutiveUnoccupiedCodes", +} + +lock_constants.COMMAND_RESULT = { + SUCCESS = "success", + FAILURE = "failure", + DUPLICATE = "duplicate", + OCCUPIED = "occupied", + INVALID_COMMAND = "invalidCommand", + RESOURCE_EXHAUSTED = "resourceExhausted", + BUSY = "busy" +} + +lock_constants.LOCK_CREDENTIALS = { + ADD = "addCredential", + UPDATE = "updateCredential", + DELETE = "deleteCredential", + DELETE_ALL = "deleteAllCredentials" +} + +lock_constants.LOCK_USERS = { + ADD = "addUser", + UPDATE = "updateUser", + DELETE = "deleteUser", + DELETE_ALL = "deleteAllUsers" +} + +lock_constants.CRED_TYPE_PIN = "pin" +lock_constants.DELAY_LOCK_EVENT = "_delay_lock_event" +lock_constants.MAX_DELAY = 10 + +return lock_constants diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils/tables.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils/tables.lua new file mode 100644 index 0000000000..bfe3562344 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils/tables.lua @@ -0,0 +1,268 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" +local st_utils = require "st.utils" +local COMMAND_RESULT = require "lock_utils.constants".COMMAND_RESULT + +local table_utils = {} + +-- DEFS describes how each capability-backed state table is structured: +-- +-- capability SmartThings capability (used for device support checks and to get latest state) +-- attribute Capability attribute function (used to emit state) +-- max_entries Attribute of the capability that defines the maximum number of entries allowed in the table +-- match_key Key used to identify entries in flat tables +-- required_keys Keys that must be non-nil when adding an entry +-- persistent_field device:set_field key used to back up the table across restarts +-- + +local DEFS = { + users = { + capability = capabilities.lockUsers, + attribute = capabilities.lockUsers.users, + max_entries = capabilities.lockUsers.totalUsersSupported, + match_key = "userIndex", + required_keys = {"userIndex", "userType"}, + persistent_field = "persistedUsers", + }, + credentials = { + capability = capabilities.lockCredentials, + attribute = capabilities.lockCredentials.credentials, + max_entries = capabilities.lockCredentials.pinUsersSupported, + match_key = "credentialIndex", + required_keys = {"userIndex", "credentialIndex", "credentialType"}, + persistent_field = "persistedCredentials", + } +} + +-- Resolve a table name to its definition. Logs an error and returns nil if unknown. +local function resolve_table_def(device, table_name) + local def = DEFS[table_name] + if not def then + device.log.error(string.format("table_helpers: unknown table %q", table_name)) + end + return def +end + +-- Validate that an entry table contains all required keys. +local function validate_entry(device, entry, required_keys) + for _, key in ipairs(required_keys or {}) do + if entry[key] == nil then + device.log.error(string.format("table_helpers: entry missing required key %q", key)) + return false + end + end + return true +end + +-- Write the current table contents to the device's persistent field store so that +-- the state survives driver restarts and can be restored if the capability state +-- cache is wiped. +local function persist_table(device, def, data) + device:set_field(def.persistent_field, st_utils.deep_copy(data), { persist = true }) +end + +-- Read the current state for a table and return a deep-copied array. +-- Accepts either a string table name ("users", "credentials") or a DEFS entry directly. +-- Returns nil (with a warning) if the capability is unsupported by the device. +-- When the capability state cache has been wiped (get_latest_state returns nil), +-- falls back to the persistent field store so that callers always receive the +-- last-known state rather than an empty table. +--- @return table[] | nil +function table_utils.get_state(device, name_or_def) + local def = type(name_or_def) == "string" and resolve_table_def(device, name_or_def) or name_or_def + if not def then return nil end + if not device:supports_capability(def.capability, "main") then + device.log.warn(string.format( + "table_helpers: device does not support capability %q", def.capability.ID + )) + return + end + local state = device:get_latest_state("main", def.capability.ID, def.attribute.NAME) + if state ~= nil then + return st_utils.deep_copy(state) + end + -- Capability state cache is absent (e.g. after a hub reboot); fall back to the + -- persistent store so that callers see the last-known table contents. + return st_utils.deep_copy(device:get_field(def.persistent_field) or {}) +end + +-- Find an entry in a named table where the match_key equals value. +-- Returns the matching entry, or nil if not found. +function table_utils.find_entry(device, table_name, value) + local def = resolve_table_def(device, table_name) + if not def then return nil end + local t = table_utils.get_state(device, def) + if not t then return nil end + for _, entry in ipairs(t) do + if entry[def.match_key] == value then return entry end + end + return nil +end + +-- Find an entry in a named table where entry[key] equals value (arbitrary key search). +-- Returns the matching entry, or nil if not found. +function table_utils.find_entry_by(device, table_name, key, value) + local def = resolve_table_def(device, table_name) + if not def then return nil end + local t = table_utils.get_state(device, def) + if not t then return nil end + for _, entry in ipairs(t) do + if entry[key] == value then return entry end + end + return nil +end + +-- Find all entries in a named table where entry[key] equals value (arbitrary key search). +-- Returns an array of matching entries, or an empty array if none found. +function table_utils.find_all_entries_by(device, table_name, key, value) + local def = resolve_table_def(device, table_name) + if not def then return {} end + local t = table_utils.get_state(device, def) + if not t then return {} end + local matches = {} + for _, entry in ipairs(t) do + if entry[key] == value then table.insert(matches, entry) end + end + return matches +end + +-- Return the lowest positive integer not yet used as the match_key in the named table. +-- Used to auto-assign the next available slot for a new entry. +function table_utils.next_index(device, table_name) + local def = resolve_table_def(device, table_name) + if not def then return 1 end + local t = table_utils.get_state(device, def) or {} + local occupied = {} + for _, entry in ipairs(t) do occupied[entry[def.match_key]] = true end + local idx = 1 + while occupied[idx] do idx = idx + 1 end + return idx +end + +function table_utils.get_max_entries(device, table_name) + local def = resolve_table_def(device, table_name) + if not def then return end + return device:get_latest_state("main", def.capability.ID, def.max_entries.NAME, 20) -- arbitrary, default to 20 if the attribute is missing +end + +-- Add an entry to a named table. The entry must satisfy all required_keys for +-- that table. An entry whose match_key value already exists in the +-- table is skipped to prevent duplicates. If the table has a max_entries limit, +-- entries that exceed the limit are not added. +function table_utils.add_entry(device, table_name, entry) + device.log.debug("table_helpers: attempting to add entry " .. st_utils.stringify_table(entry) .. " to " .. table_name) + local def = resolve_table_def(device, table_name) + if not def then return COMMAND_RESULT.FAILURE end + if not validate_entry(device, entry, def.required_keys) then return COMMAND_RESULT.FAILURE end + local t = table_utils.get_state(device, def) + if not t then return COMMAND_RESULT.FAILURE end + + if #t >= table_utils.get_max_entries(device, table_name) then + device.log.warn(string.format( + "table_helpers: cannot add entry to %q, max entries reached", table_name + )) + return COMMAND_RESULT.RESOURCE_EXHAUSTED + end + + -- Object entry: skip if an entry with the same match_key value already exists + if def.match_key then + for _, existing in ipairs(t) do + if existing[def.match_key] == entry[def.match_key] then + device.log.warn(string.format( + "table_helpers: entry with %s == %s already exists in %q, skipping", + def.match_key, tostring(entry[def.match_key]), table_name + )) + return COMMAND_RESULT.OCCUPIED + end + end + end + + table.insert(t, st_utils.deep_copy(entry)) + + device:emit_event(def.attribute(t, {visibility = {displayed = false}})) + persist_table(device, def, t) + return COMMAND_RESULT.SUCCESS +end + + +--- Update fields of an existing entry in a table. +--- The entry to update is identified by the match_key parameter in DEFS. +function table_utils.update_entry(device, table_name, match_value, updates) + device.log.debug("table_helpers: attempting to update entry " .. match_value .. " in " .. table_name .. ": " .. st_utils.stringify_table(updates)) + local def = resolve_table_def(device, table_name) + if not def then return COMMAND_RESULT.FAILURE end + local t = table_utils.get_state(device, def) + if not t then return COMMAND_RESULT.FAILURE end + + for _, entry in ipairs(t) do + if entry[def.match_key] == match_value then + for k, v in pairs(updates) do + entry[k] = v + end + device:emit_event(def.attribute(t, {visibility = {displayed = false}})) + persist_table(device, def, t) + return COMMAND_RESULT.SUCCESS + end + end + + device.log.warn(string.format( + "table_helpers: no entry found in %q with %s == %s", + table_name, def.match_key, tostring(match_value) + )) + return COMMAND_RESULT.FAILURE +end + + +-- Delete an entry from a table. +-- +-- Returns SUCCESS, or FAILURE if nothing matched. +function table_utils.delete_entry(device, table_name, matcher) + device.log.debug("table_helpers: attempting to delete entry " .. matcher .. " from " .. table_name) + local def = resolve_table_def(device, table_name) + if not def then return COMMAND_RESULT.FAILURE end + local t = table_utils.get_state(device, def) + if not t then return COMMAND_RESULT.FAILURE end + + local predicate = function(entry) return entry[def.match_key] == matcher end + + for i, entry in ipairs(t) do + if predicate(entry) then + table.remove(t, i) + device:emit_event(def.attribute(t, {visibility = {displayed = false}})) + persist_table(device, def, t) + return COMMAND_RESULT.SUCCESS + end + end + return COMMAND_RESULT.FAILURE +end + +-- Delete all entries from a table. +function table_utils.delete_all_entries(device, table_name) + device.log.debug("table_helpers: attempting to delete all entries from " .. table_name) + local def = resolve_table_def(device, table_name) + if not def then return COMMAND_RESULT.FAILURE end + device:emit_event(def.attribute({}, {visibility = {displayed = false}})) + persist_table(device, def, {}) + return COMMAND_RESULT.SUCCESS +end + +-- Restore capability state from the persistent field store. +-- Called during init to re-emit table events if the capability state cache +-- has been wiped (e.g. after a hub reboot). Only emits for tables that have +-- persisted data, are in a nil state, and whose capability is supported by the device. +function table_utils.restore_from_persistent_store(device) + for _, internal in pairs(DEFS) do + if device:supports_capability(internal.capability, "main") and + device:get_latest_state("main", internal.capability.ID, internal.attribute.NAME) == nil + then + local persisted = st_utils.deep_copy(device:get_field(internal.persistent_field)) + if persisted and #persisted > 0 then + device:emit_event(internal.attribute(persisted, {visibility = {displayed = false}})) + end + end + end +end + +return table_utils diff --git a/drivers/SmartThings/zigbee-lock/src/lock_utils/utils.lua b/drivers/SmartThings/zigbee-lock/src/lock_utils/utils.lua new file mode 100644 index 0000000000..90bdd96f33 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/lock_utils/utils.lua @@ -0,0 +1,66 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local consts = require "lock_utils.constants" + +local lock_utils = {} + +-- [[ BUSY STATE MANAGEMENT ]] -- + +-- Check if we are currently busy performing a task, or at least 10 seconds have passed since the busy state was last set. +-- If busy, return true. If not busy, clear any stale state and return false. +function lock_utils.is_device_busy(device) + local c_time = os.time() + local busy_since = device:get_field(consts.DRIVER_STATE.BUSY) or false + + if (busy_since == false) or (c_time - busy_since > 10) then + lock_utils.clear_busy_state(device) + return false + end + return true +end + +-- Set states that may be required when in busy state +function lock_utils.set_busy_state(device, command_name, command_args) + device:set_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS, command_name) + device:set_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, command_args or {}) + device:set_field(consts.DRIVER_STATE.BUSY, os.time()) +end + +-- Clear states that were set when in busy state +function lock_utils.clear_busy_state(device) + device:set_field(consts.DRIVER_STATE.COMMAND_IN_PROGRESS, nil) + device:set_field(consts.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, nil) + device:set_field(consts.DRIVER_STATE.BUSY, false) +end + + +-- [[ SYNC STATE MANAGEMENT ]] -- + +function lock_utils.sync_device_state(device) + local clusters = require "st.zigbee.zcl.clusters" + -- Per spec, this attribute should be a boolean set to True if it is ok for the door lock server to send PINs over the air. + device:send(clusters.DoorLock.attributes.SendPINOverTheAir:write(device, true)) + + -- if consts.SYNC.CODE_INDEX is nil, we haven't started syncing codes from the lock yet, + -- so start the process from 1. note: the Zigbee "Master Code" is associated with index 0. + if (device:get_field(consts.SYNC.CODE_INDEX) == nil) then + device:set_field(consts.SYNC.CODE_INDEX, 1) + end + lock_utils.set_busy_state(device, consts.SYNC.CODES_FROM_LOCK) + device:send(clusters.DoorLock.server.commands.GetPINCode(device, device:get_field(consts.SYNC.CODE_INDEX))) +end + + +-- [[ COMMAND RESULT STATE MANAGEMENT ]] -- + +function lock_utils.emit_command_result(device, capability, command_name, status_code, additional_info) + local info = additional_info or {} + info.commandName = command_name + info.statusCode = status_code + if capability then + device:emit_event(capability.commandResult(info, {state_change = true, visibility = {displayed = false}})) + end +end + +return lock_utils diff --git a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua index fff290df5d..e09a598efa 100644 --- a/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/samsungsds/init.lua @@ -10,7 +10,9 @@ local cluster_base = require "st.zigbee.cluster_base" local PowerConfiguration = clusters.PowerConfiguration local DoorLock = clusters.DoorLock local Lock = capabilities.lock -local lock_utils = require "lock_utils" +local consts = require "lock_utils.constants" +local tables = require "lock_utils.tables" +local socket = require "cosock.socket" local SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND = 0x1F local SAMSUNG_SDS_MFR_CODE = 0x0003 @@ -54,7 +56,8 @@ local function emit_event_if_latest_state_missing(device, component, capability, end local device_added = function(self, device) - lock_utils.populate_state_from_data(device) + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) -- set migrated for all Samsung SDS devices. They do not require any legacy functionality. + emit_event_if_latest_state_missing(device, "main", capabilities.lock, capabilities.lock.lock.NAME, capabilities.lock.lock.unlocked()) device:emit_event(capabilities.battery.battery(100)) end @@ -68,10 +71,86 @@ end local battery_init = battery_defaults.build_linear_voltage_init(4.0, 6.0) local device_init = function(driver, device, event) + device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) -- set migrated for all Samsung SDS devices. They do not require any legacy functionality. battery_init(driver, device, event) device:remove_monitored_attribute(clusters.PowerConfiguration.ID, clusters.PowerConfiguration.attributes.BatteryVoltage.ID) device:remove_configured_attribute(clusters.PowerConfiguration.ID, clusters.PowerConfiguration.attributes.BatteryVoltage.ID) - lock_utils.populate_state_from_data(device) +end + +local operating_event_notification = function(driver, device, zb_rx) + local op_event_code = tonumber(zb_rx.body.zcl_body.operation_event_code.value) + local op_event_source = tonumber(zb_rx.body.zcl_body.operation_event_source.value) + + -- get lock event or return + local OpEventCode = clusters.DoorLock.types.OperationEventCode + local OP_EVENT_CODE_CAPABILITY_MAP = { + [OpEventCode.LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.UNLOCK] = capabilities.lock.lock.unlocked(), + [OpEventCode.ONE_TOUCH_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.KEY_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.KEY_UNLOCK] = capabilities.lock.lock.unlocked(), + [OpEventCode.AUTO_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.MANUAL_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.MANUAL_UNLOCK] = capabilities.lock.lock.unlocked(), + [OpEventCode.SCHEDULE_LOCK] = capabilities.lock.lock.locked(), + [OpEventCode.SCHEDULE_UNLOCK] = capabilities.lock.lock.unlocked() + } + local lock_event = OP_EVENT_CODE_CAPABILITY_MAP[op_event_code] + if not lock_event then return end + lock_event.data = {} + + -- get method of lock event + local OpEventSource = clusters.DoorLock.types.DrlkOperationEventSource + local OP_EVENT_SOURCE_CAPABILITY_MAP = { + [OpEventSource.KEYPAD] = "keypad", + [OpEventSource.RF] = "command", + [OpEventSource.MANUAL] = "manual", + [OpEventSource.RFID] = "rfid", + -- These last two sources are not found in the spec, but they were in + -- the legacy driver and appear to be related to the Samsung SDS + [4] = "fingerprint", + [5] = "bluetooth", + } + if (op_event_source ~= OpEventSource.KEYPAD and ( + op_event_code == OpEventCode.AUTO_LOCK or + op_event_code == OpEventCode.SCHEDULE_LOCK or + op_event_code == OpEventCode.SCHEDULE_UNLOCK + )) then + lock_event.data.method = "auto" + else + lock_event.data.method = OP_EVENT_SOURCE_CAPABILITY_MAP[op_event_source] or "manual" + end + + -- get stored lockUsers data if applicable + if op_event_source == OpEventSource.KEYPAD and device:supports_capability(capabilities.lockUsers) then + local user_id = tonumber(zb_rx.body.zcl_body.user_id.value) + local associated_user = tables.find_entry(device, "users", user_id) + if associated_user then + lock_event.data.userIndex = user_id .. "" + lock_event.data.userName = associated_user.userName + lock_event.data.userType = associated_user.userType + else + lock_event.data.userIndex = user_id .. "" + lock_event.data.userName = "Guest " .. user_id -- default + end + end + + -- if this is an event corresponding to a recently-received attribute report, we + -- want to set our delay timer for future lock attribute report events + local endpoint_id = zb_rx.address_header.src_endpoint.value + if lock_event.value.value == device:get_latest_state( + device:get_component_id_for_endpoint(endpoint_id), + capabilities.lock.ID, + capabilities.lock.lock.ID + ) then + local preceding_event_time = device:get_field(consts.DELAY_LOCK_EVENT) or 0 + local time_diff = socket.gettime() - preceding_event_time + if time_diff < consts.MAX_DELAY then + device:set_field(consts.DELAY_LOCK_EVENT, time_diff) + end + end + + device:emit_event_for_endpoint(endpoint_id, lock_event) end local samsung_sds_driver = { @@ -79,7 +158,8 @@ local samsung_sds_driver = { zigbee_handlers = { cluster = { [DoorLock.ID] = { - [SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND] = mfg_lock_door_handler + [SAMSUNG_SDS_MFR_SPECIFIC_UNLOCK_COMMAND] = mfg_lock_door_handler, + [clusters.DoorLock.client.commands.OperatingEventNotification.ID] = operating_event_notification, } }, attr = { diff --git a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua index ff4bf8980d..d3491ed664 100644 --- a/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-lock/src/sub_drivers.lua @@ -3,9 +3,9 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { + lazy_load_if_possible("legacy-handlers"), lazy_load_if_possible("samsungsds"), - lazy_load_if_possible("yale"), + lazy_load_if_possible("bad-battery-reporter"), lazy_load_if_possible("yale-fingerprint-lock"), - lazy_load_if_possible("lock-without-codes"), } return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_bad_battery_reporter.lua similarity index 59% rename from drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua rename to drivers/SmartThings/zigbee-lock/src/test/test_bad_battery_reporter.lua index fcae7eda03..61fb28ce8d 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_yale_fingerprint_bad_battery_reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_bad_battery_reporter.lua @@ -50,4 +50,42 @@ test.register_message_test( } ) +local mock_device_yrd = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zigbee_endpoints ={ + [1] = { + id = 1, + manufacturer ="Yale", + model ="YRD220/240 TSDB", + server_clusters = {} + } + } +}) + +local function test_init_yrd() + test.mock_device.add_test_device(mock_device_yrd) +end + +test.register_message_test( + "Battery percentage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_device_yrd.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device_yrd, 55) } + }, + { + channel = "capability", + direction = "send", + message = mock_device_yrd:generate_test_message("main", capabilities.battery.battery(55)) + } + }, + { + test_init = test_init_yrd, + min_api_version = 17 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_generic_fingerprint_profile_update.lua similarity index 56% rename from drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua rename to drivers/SmartThings/zigbee-lock/src/test/test_generic_fingerprint_profile_update.lua index 4580101cd0..e6d6a22b78 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_generic_lock_migration.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_generic_fingerprint_profile_update.lua @@ -6,6 +6,7 @@ local test = require "integration_test" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local PowerConfiguration = clusters.PowerConfiguration local DoorLock = clusters.DoorLock @@ -31,6 +32,18 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device)}) test.socket.zigbee:__queue_receive({mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 8)}) mock_device:expect_metadata_update({profile = "base-lock"}) + + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = t_utils.get_profile_definition("base-lock.yml") })) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + -- inject_capability_command(refresh) is called inline by info_changed + test.socket.zigbee:__expect_send({mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device)}) + test.socket.zigbee:__expect_send({mock_device.id, DoorLock.attributes.LockState:read(mock_device)}) + test.socket.zigbee:__expect_send({mock_device.id, Alarm.attributes.AlarmCount:read(mock_device)}) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.MinPINCodeLength:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device) }) end, { min_api_version = 17 diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_init_lifecycle_handlers.lua b/drivers/SmartThings/zigbee-lock/src/test/test_init_lifecycle_handlers.lua new file mode 100644 index 0000000000..1ff448a719 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_init_lifecycle_handlers.lua @@ -0,0 +1,484 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Integration tests for the four lifecycle handlers defined in init.lua: +-- added (device_added), doConfigure (do_configure), +-- infoChanged (info_changed), init (LockLifecycle.init) + +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local DoorLock = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration +local Alarms = clusters.Alarms +local constants = require "lock_utils.constants" + +-- ── Shared mock devices ──────────────────────────────────────────────────── +-- base-lock profile: lock + lockCodes + lockCredentials + lockUsers + battery +local mock_device_base = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), +}) + +-- Same profile but provisioning_state = "TYPED" (freshly fingerprinted) +local mock_device_typed = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + _provisioning_state = "TYPED", +}) + +-- lock-battery profile: lock + battery only (no lockCodes / lockCredentials) +local mock_device_battery = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("lock-battery.yml"), +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +-- Helper: make a test_init function that suppresses startup messages and +-- registers the given device. +local function make_test_init(device) + return function() + test.disable_startup_messages() + test.mock_device.add_test_device(device) + end +end + +-- Helper: expect the five zigbee messages produced by sync_device_state on a +-- freshly-started device (no cached capability state, CODE_INDEX starts at 1). +local function expect_sync_device_state(device) + test.socket.zigbee:__expect_send({ device.id, DoorLock.attributes.SendPINOverTheAir:write(device, true) }) + test.socket.zigbee:__expect_send({ device.id, DoorLock.server.commands.GetPINCode(device, 1) }) +end + +-- Helper: expect the messages produced by the legacy reload_all_codes path after +-- the 2-second doConfigure timer fires on a non-SLGA_MIGRATED device. +-- Unlike sync_device_state, reload_all_codes emits scanCodes("Scanning") and +-- starts iterating from code slot 0 (CHECKING_CODE = 0). +local function expect_reload_all_codes_messages(device) + test.socket.zigbee:__expect_send({ device.id, DoorLock.attributes.SendPINOverTheAir:write(device, true) }) + test.socket.zigbee:__expect_send({ device.id, DoorLock.attributes.MaxPINCodeLength:read(device) }) + test.socket.zigbee:__expect_send({ device.id, DoorLock.attributes.MinPINCodeLength:read(device) }) + test.socket.zigbee:__expect_send({ device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(device) }) + test.socket.capability:__expect_send( + device:generate_test_message("main", + capabilities.lockCodes.scanCodes("Scanning", { visibility = { displayed = false } })) + ) + test.socket.zigbee:__expect_send({ device.id, DoorLock.server.commands.GetPINCode(device, 0) }) +end + +-- Helper: expect the six zigbee messages sent by do_configure for any device. +local function expect_do_configure_zigbee(device) + test.socket.zigbee:__expect_send({ + device.id, + zigbee_test_utils.build_bind_request(device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID), + }) + test.socket.zigbee:__expect_send({ + device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(device, 600, 21600, 1), + }) + test.socket.zigbee:__expect_send({ + device.id, + zigbee_test_utils.build_bind_request(device, zigbee_test_utils.mock_hub_eui, DoorLock.ID), + }) + test.socket.zigbee:__expect_send({ + device.id, + DoorLock.attributes.LockState:configure_reporting(device, 0, 3600, 0), + }) + test.socket.zigbee:__expect_send({ + device.id, + zigbee_test_utils.build_bind_request(device, zigbee_test_utils.mock_hub_eui, Alarms.ID), + }) + test.socket.zigbee:__expect_send({ + device.id, + Alarms.attributes.AlarmCount:configure_reporting(device, 0, 21600, 0), + }) +end + +-- ============================================================================ +-- added (device_added) +-- ============================================================================ + +test.register_coroutine_test( + "added: TYPED device with lockCodes emits migrated event, persists SLGA_MIGRATED, and injects refresh", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "added" }) + + -- Migrated event is emitted before the injected refresh + test.socket.capability:__expect_send( + mock_device_typed:generate_test_message("main", + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + ) + -- inject_capability_command calls the refresh handler inline + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + DoorLock.attributes.LockState:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + Alarms.attributes.AlarmCount:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + DoorLock.attributes.MaxPINCodeLength:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + DoorLock.attributes.MinPINCodeLength:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device_typed), + }) + test.wait_for_events() + + assert( + mock_device_typed:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true, + "SLGA_MIGRATED must be true after added fires for a TYPED device" + ) + end, + { test_init = make_test_init(mock_device_typed) } +) + +test.register_coroutine_test( + "added: non-TYPED (PROVISIONED) device with lockCodes does NOT emit migrated but still injects refresh", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_base.id, "added" }) + + -- No migrated capability event expected + test.socket.zigbee:__expect_send({ + mock_device_base.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_base), + }) + test.socket.zigbee:__expect_send({ + mock_device_base.id, + DoorLock.attributes.LockState:read(mock_device_base), + }) + test.socket.zigbee:__expect_send({ + mock_device_base.id, + Alarms.attributes.AlarmCount:read(mock_device_base), + }) + test.wait_for_events() + + assert( + mock_device_base:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) ~= true, + "SLGA_MIGRATED must NOT be set for a non-TYPED device" + ) + end, + { test_init = make_test_init(mock_device_base) } +) + +test.register_coroutine_test( + "added: device without lockCodes does NOT emit migrated event but still injects refresh", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) + + -- No migrated capability event expected. + -- For non-SLGA_MIGRATED devices the legacy-handlers refresh fires, which reads + -- NumberOfPINUsersSupported when the device has no lockCodes and no cached code support. + test.socket.zigbee:__expect_send({ + mock_device_battery.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_battery), + }) + test.socket.zigbee:__expect_send({ + mock_device_battery.id, + DoorLock.attributes.LockState:read(mock_device_battery), + }) + test.socket.zigbee:__expect_send({ + mock_device_battery.id, + Alarms.attributes.AlarmCount:read(mock_device_battery), + }) + test.socket.zigbee:__expect_send({ + mock_device_battery.id, + DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device_battery), + }) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_battery) } +) + +-- ============================================================================ +-- doConfigure (do_configure) +-- ============================================================================ + +test.register_coroutine_test( + "doConfigure: SLGA_MIGRATED device with lockCredentials sends bind/configure then calls sync_device_state after 2-second delay", + function() + -- Pre-seed SLGA_MIGRATED so the main driver's do_configure fires (not legacy-handlers). + mock_device_base:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device_base.id, "doConfigure" }) + + expect_do_configure_zigbee(mock_device_base) + mock_device_base:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + -- Timer fires: sync_device_state is called (GetPINCode slot 1) + test.mock_time.advance_time(2) + test.socket.zigbee:__set_channel_ordering("relaxed") + expect_sync_device_state(mock_device_base) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_base) } +) + +test.register_coroutine_test( + "doConfigure: SLGA_MIGRATED device without lockCredentials sends bind/configure but does NOT create sync timer", + function() + -- Pre-seed SLGA_MIGRATED so the main driver's do_configure fires. + mock_device_battery:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "doConfigure" }) + + expect_do_configure_zigbee(mock_device_battery) + mock_device_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + -- lock-battery has no lockCredentials, so no 2-second timer is created. + end, + { test_init = make_test_init(mock_device_battery) } +) + +test.register_coroutine_test( + "doConfigure: non-SLGA_MIGRATED device triggers legacy reloadAllCodes (with scanCodes emit) after 2-second delay", + function() + -- mock_device_typed has no SLGA_MIGRATED → legacy-handlers' do_configure fires, + -- which injects a reloadAllCodes capability command after a 2-second delay. + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "doConfigure" }) + + expect_do_configure_zigbee(mock_device_typed) + mock_device_typed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + -- Timer fires: legacy reload_all_codes runs, iterating from code slot 0. + test.mock_time.advance_time(2) + test.socket.zigbee:__set_channel_ordering("relaxed") + expect_reload_all_codes_messages(mock_device_typed) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_typed) } +) + +-- ============================================================================ +-- infoChanged (info_changed) +-- Each test uses a per-test fresh device (upvalue pattern) to avoid +-- raw_st_data contamination from generate_info_changed across tests. +-- init is triggered first so the driver loads the device into device_cache, +-- allowing infoChanged to correctly identify the old profile. +-- ============================================================================ + +do + local dev + test.register_coroutine_test( + "infoChanged: switching from non-lockCodes to lockCodes+lockCredentials profile triggers full SLGA migration and two syncs", + function() + -- Warm up device_cache with the original (lock-battery) profile via init. + -- For a non-SLGA_MIGRATED device, legacy-handlers' init fires (no zigbee sends). + test.socket.device_lifecycle:__queue_receive({ dev.id, "init" }) + test.wait_for_events() + + -- Switch to base-lock (lockCodes + lockCredentials) + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.device_lifecycle:__queue_receive( + dev:generate_info_changed({ profile = t_utils.get_profile_definition("base-lock.yml") }) + ) + -- Migration events + test.socket.capability:__expect_send( + dev:generate_test_message("main", + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + dev:generate_test_message("main", + capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) + ) + -- inject_capability_command calls the refresh handler inline + test.socket.zigbee:__expect_send({ + dev.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(dev), + }) + test.socket.zigbee:__expect_send({ + dev.id, + DoorLock.attributes.LockState:read(dev), + }) + test.socket.zigbee:__expect_send({ + dev.id, + Alarms.attributes.AlarmCount:read(dev), + }) + test.socket.zigbee:__expect_send({ + dev.id, + DoorLock.attributes.MaxPINCodeLength:read(dev), + }) + test.socket.zigbee:__expect_send({ + dev.id, + DoorLock.attributes.MinPINCodeLength:read(dev), + }) + test.socket.zigbee:__expect_send({ + dev.id, + DoorLock.attributes.NumberOfPINUsersSupported:read(dev), + }) + -- Immediate sync_device_state + test.wait_for_events() + + assert( + dev:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true, + "SLGA_MIGRATED must be set after infoChanged profile switch" + ) + + -- Delayed sync_device_state (2 s) + test.mock_time.advance_time(2) + test.socket.zigbee:__set_channel_ordering("relaxed") + expect_sync_device_state(dev) + test.wait_for_events() + end, + { + test_init = function() + test.disable_startup_messages() + dev = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("lock-battery.yml"), + }) + test.mock_device.add_test_device(dev) + end, + } + ) +end + +do + local dev + test.register_coroutine_test( + "infoChanged: no profile change does nothing", + function() + -- Warm up device_cache + test.socket.device_lifecycle:__queue_receive({ dev.id, "init" }) + test.wait_for_events() + + -- infoChanged with no profile change + test.socket.device_lifecycle:__queue_receive(dev:generate_info_changed({})) + test.wait_for_events() + -- No capability events, no zigbee sends expected + end, + { + test_init = function() + test.disable_startup_messages() + dev = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + }) + test.mock_device.add_test_device(dev) + end, + } + ) +end + + +-- ============================================================================ +-- init (LockLifecycle.init) +-- ============================================================================ + +test.register_coroutine_test( + "init: device with lockCodes and SLGA_MIGRATED=true emits migrated + supportedCredentials", + function() + mock_device_base:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_base.id, "init" }) + test.socket.capability:__expect_send( + mock_device_base:generate_test_message("main", + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device_base:generate_test_message("main", + capabilities.lockCredentials.supportedCredentials({ "pin" }, { visibility = { displayed = false } })) + ) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_base) } +) + +test.register_coroutine_test( + "init: device with lockCodes but SLGA_MIGRATED not set does nothing", + function() + -- SLGA_MIGRATED is not set; lockCodes is supported; elseif branch does not apply + test.socket.device_lifecycle:__queue_receive({ mock_device_base.id, "init" }) + test.wait_for_events() + -- No capability events and no zigbee sends expected + end, + { test_init = make_test_init(mock_device_base) } +) + +test.register_coroutine_test( + "init: SLGA_MIGRATED device without lockCodes sends NumberOfPINUsersSupported read to detect re-profiling", + function() + -- Pre-seed SLGA_MIGRATED=true so legacy-handlers is bypassed and the main driver's + -- init fires. lock-battery has no lockCodes, so the elseif branch executes and + -- reads NumberOfPINUsersSupported to detect whether the device should be re-profiled. + mock_device_battery:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) + + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) + test.socket.zigbee:__expect_send({ + mock_device_battery.id, + DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device_battery), + }) + test.wait_for_events() + end, + { test_init = make_test_init(mock_device_battery) } +) + +-- ============================================================================ +-- driverSwitched (LockLifecycle.driver_switched) +-- ============================================================================ + +test.register_coroutine_test( + "driver_switched: device with lockCodes and migrated=true persists SLGA_MIGRATED and updates metadata", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "added" }) + + -- Migrated event is emitted before the injected refresh + test.socket.capability:__expect_send( + mock_device_typed:generate_test_message("main", + capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) + ) + -- inject_capability_command calls the refresh handler inline + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + DoorLock.attributes.LockState:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + Alarms.attributes.AlarmCount:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + DoorLock.attributes.MaxPINCodeLength:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + DoorLock.attributes.MinPINCodeLength:read(mock_device_typed), + }) + test.socket.zigbee:__expect_send({ + mock_device_typed.id, + DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device_typed), + }) + test.wait_for_events() + + -- driverSwitched occurs after added, so migrated=true is already set in the capability state cache + test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "driver_switched" }) + mock_device_typed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + test.wait_for_events() + + assert(mock_device_typed:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true) + end, + { test_init = make_test_init(mock_device_typed) } +) + +-- ============================================================================ +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_lock_code_slga_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_lock_code_slga_migration.lua new file mode 100644 index 0000000000..e29788fe36 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_lock_code_slga_migration.lua @@ -0,0 +1,76 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local clusters = require "st.zigbee.zcl.clusters" +local DoorLock = clusters.DoorLock +local PowerConfiguration = clusters.PowerConfiguration +local Alarm = clusters.Alarms +local capabilities = require "st.capabilities" +local constants = require "lock_utils.constants" + +local json = require "st.json" + +local mock_datastore = require "integration_test.mock_env_datastore" + +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("base-lock.yml"), + data = { + lockCodes = json.encode({ + ["1"] = "Zach", + ["5"] = "Steven" + }), + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "Device called 'migrate' command", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.wait_for_events() + -- Validate lockCodes field + mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["5"] = "Steven" }) + -- Validate migration complete flag + mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) + -- Set min/max code length attributes + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 5) }) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 10) }) + test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 4) }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.minCodeLength(5, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodeLength(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(4, { visibility = { displayed = false } }))) + test.wait_for_events() + -- Validate `migrate` command functionality. + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(5, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(10, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(4, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({{userIndex=1, userName="Zach", userType="guest"}, {userIndex=5, userName="Steven", userType="guest"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({{credentialIndex=1, credentialType="pin", userIndex=1, credentialName="Zach"}, {credentialIndex=5, credentialType="pin", userIndex=5, credentialName="Steven"}}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) + test.wait_for_events() + assert(mock_device:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true, "SLGA_MIGRATED field should be set to true after migration") + end +) + +test.run_registered_tests() \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_lock_credentials_commands.lua b/drivers/SmartThings/zigbee-lock/src/test/test_lock_credentials_commands.lua new file mode 100644 index 0000000000..33293e784d --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_lock_credentials_commands.lua @@ -0,0 +1,578 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Integration tests for the lockCredentials capability commands: +-- addCredential, updateCredential, deleteCredential, deleteAllCredentials + +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local DoorLock = clusters.DoorLock +local table_utils = require "lock_utils.tables" +local constants = require "lock_utils.constants" + +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType +local SetCodeStatus = DoorLock.types.DrlkSetCodeStatus +local ResponseStatus = DoorLock.types.DrlkPassFailStatus + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), +}) +zigbee_test_utils.prepare_zigbee_env_info() + +-- ── helpers ──────────────────────────────────────────────────────────────── + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +local function seed_users(entries) + for _, entry in ipairs(entries) do + local so_far = {} + for _, e in ipairs(entries) do + table.insert(so_far, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(so_far, { visibility = { displayed = false } })) + ) + assert(table_utils.add_entry(mock_device, "users", entry) == constants.COMMAND_RESULT.SUCCESS) + end + test.wait_for_events() +end + +local function seed_credentials(entries) + for _, entry in ipairs(entries) do + local so_far = {} + for _, e in ipairs(entries) do + table.insert(so_far, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials(so_far, { visibility = { displayed = false } })) + ) + assert(table_utils.add_entry(mock_device, "credentials", entry) == constants.COMMAND_RESULT.SUCCESS) + end + test.wait_for_events() +end + +-- ============================================================================ +-- addCredential +-- ============================================================================ + +test.register_coroutine_test( + "addCredential: sends SetPINCode to the lock and emits success when the lock acknowledges", + function() + -- Queue the capability command: userIndex=1, userType="guest", credentialType="pin", credentialData="1234" + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 1, "guest", "pin", "1234" } }, + }) + + -- Expect SetPINCode to be sent to the lock + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"), + }) + test.wait_for_events() + + -- Lock responds with SUCCESS + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.SUCCESS), + }) + + -- Handler adds the credential and user to the credentials table + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + -- commandResult with userIndex and credentialIndex + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential: emits failure when the lock returns GENERAL_FAILURE", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 1, "guest", "pin", "1234" } }, + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.GENERAL_FAILURE), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential: emits resourceExhausted when the lock returns MEMORY_FULL", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 2, "guest", "pin", "5678" } }, + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 2, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "5678"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.MEMORY_FULL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "resourceExhausted" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential: emits duplicate when the lock returns DUPLICATE_CODE", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 1, "guest", "pin", "1234" } }, + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.DUPLICATE_CODE), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "duplicate" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential: returns busy when another operation is already in progress", + function() + -- Put the device into busy state by starting an addCredential operation + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 1, "guest", "pin", "1234" } }, + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"), + }) + test.wait_for_events() + + -- Second addCredential while first is still pending → should get busy + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 2, "guest", "pin", "5678" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- updateCredential +-- ============================================================================ + +test.register_coroutine_test( + "updateCredential: sends SetPINCode and emits success when the lock acknowledges", + function() + seed_credentials({ { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 1, 1, "pin", "newPin9" } }, + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "newPin9"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.SUCCESS), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateCredential: returns failure immediately when the credential does not exist in the table", + function() + -- No credentials seeded — credentialIndex 99 does not exist + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 99, 99, "pin", "badPin" } }, + }) + + -- No zigbee message should be sent + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateCredential: returns busy when another operation is already in progress", + function() + seed_credentials({ { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }) + + -- Start first updateCredential to make device busy + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 1, 1, "pin", "pin1111" } }, + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin1111"), + }) + test.wait_for_events() + + -- Second updateCredential while first is pending + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 1, 1, "pin", "pin2222" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteCredential +-- ============================================================================ + +test.register_coroutine_test( + "deleteCredential: sends ClearPINCode and emits success with indices when the lock returns PASS", + function() + seed_users({ { userIndex = 1, userName = "Alice", userType = "guest" } }) + seed_credentials({ { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearPINCodeResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + + -- credential table and user should be deleted + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteCredential: emits failure when the lock returns FAIL", + function() + seed_credentials({ { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearPINCodeResponse.build_test_rx(mock_device, ResponseStatus.FAIL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteCredential: returns failure immediately when the credentialIndex does not exist in the table", + function() + -- No credentials seeded — index 5 is unknown + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 5, "pin" } }, + }) + + -- No zigbee messages should be sent + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteCredential: returns busy when another operation is already in progress", + function() + seed_credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + }) + + -- Start first deleteCredential to make device busy + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) + test.wait_for_events() + + -- Second deleteCredential while first is pending → busy + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 2, "pin" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteAllCredentials +-- ============================================================================ + +test.register_coroutine_test( + "deleteAllCredentials: sends ClearAllPINCodes and emits success when the lock returns PASS", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + }) + seed_credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearAllPINCodesResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + + -- deleteAllCredentials only clears credentials; users table is untouched + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllCredentials: emits failure when the lock returns FAIL", + function() + seed_credentials({ { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearAllPINCodesResponse.build_test_rx(mock_device, ResponseStatus.FAIL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllCredentials: returns busy when another operation is already in progress", + function() + seed_credentials({ { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }) + + -- First deleteAllCredentials starts the zigbee flow (device is now busy) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + -- Second deleteAllCredentials while first is pending → busy + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "busy" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_lock_pre_configured.lua b/drivers/SmartThings/zigbee-lock/src/test/test_lock_pre_configured.lua new file mode 100644 index 0000000000..0eb4ac396f --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_lock_pre_configured.lua @@ -0,0 +1,653 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Integration tests for lockUsers and lockCredentials commands with state +-- pre-configured before each test. Two users and two credentials are seeded +-- at the start of every test so tests can focus on the various response states +-- produced by the zigbee response handlers in commands.lua. + +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local DoorLock = clusters.DoorLock +local table_utils = require "lock_utils.tables" +local constants = require "lock_utils.constants" + +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType +local SetCodeStatus = DoorLock.types.DrlkSetCodeStatus +local ResponseStatus = DoorLock.types.DrlkPassFailStatus + +-- ── Shared device ────────────────────────────────────────────────────────── + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), +}) +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +-- ── Seeding helpers ──────────────────────────────────────────────────────── + +local INITIAL_USERS = { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, +} +local INITIAL_CREDS = { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, +} + +-- Seed a list of entries into a named table, consuming the resulting events. +local function seed_table(attribute_fn, table_name, entries) + local accumulated = {} + for _, entry in ipairs(entries) do + table.insert(accumulated, entry) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + attribute_fn(accumulated, { visibility = { displayed = false } })) + ) + assert( + table_utils.add_entry(mock_device, table_name, entry) == constants.COMMAND_RESULT.SUCCESS, + "seed_table: add_entry failed for " .. table_name + ) + end + test.wait_for_events() +end + +-- Pre-configure each test with 2 users and 2 credentials. +local function setup_state() + seed_table(capabilities.lockUsers.users, "users", INITIAL_USERS) + seed_table(capabilities.lockCredentials.credentials, "credentials", INITIAL_CREDS) +end + +-- ============================================================================ +-- addUser — pre-configured device (2 users already present) +-- ============================================================================ + +test.register_coroutine_test( + "addUser (pre-configured): assigns the next available index (3) when two users already exist", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Carol", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 3 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- updateUser — pre-configured device +-- ============================================================================ + +test.register_coroutine_test( + "updateUser (pre-configured): updates Alice's name and emits success with userIndex", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 1, "AliceRenamed", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "AliceRenamed", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + + +-- ============================================================================ +-- deleteUser with associated credential — exercises clear_pin_code_response +-- ============================================================================ + +test.register_coroutine_test( + "deleteUser (pre-configured, PASS): removes both user and credential and emits success for each", + function() + setup_state() + + -- Delete user 1 who has credential 1 → driver injects deleteCredential → ClearPINCode + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 1 } }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearPINCodeResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 2, credentialIndex = 2, credentialType = "pin" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 2, userName = "Bob", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteUser (pre-configured, FAIL): emits failure for both capabilities when the lock rejects", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 2 } }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 2) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearPINCodeResponse.build_test_rx(mock_device, ResponseStatus.FAIL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- addCredential response states — set_pin_code_response +-- ============================================================================ + +test.register_coroutine_test( + "addCredential (pre-configured): succeeds for a new slot and emits userIndex + credentialIndex", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 3, "guest", "pin", "pin03" } }, + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin03"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.SUCCESS), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 3, userName = "Guest 3", userType = "guest" }, -- default name since lock doesn't provide one + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin", credentialName = "Guest 3" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", userIndex = 3, credentialIndex = 3 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential (pre-configured): emits duplicate when the lock rejects with DUPLICATE_CODE", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 3, "guest", "pin", "pin01" } }, + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin01"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.DUPLICATE_CODE), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "duplicate" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addCredential (pre-configured): emits resourceExhausted when the lock returns MEMORY_FULL", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 3, "guest", "pin", "pin03" } }, + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 3, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "pin03"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.MEMORY_FULL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "resourceExhausted" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- updateCredential response states — set_pin_code_response +-- ============================================================================ + +test.register_coroutine_test( + "updateCredential (pre-configured): succeeds and emits success with userIndex and credentialIndex", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 1, 1, "pin", "newPin1" } }, + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "newPin1"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.SUCCESS), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateCredential (pre-configured): returns failure immediately for a non-existent credentialIndex", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 99, 99, "pin", "badPin" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteCredential standalone (lockCredentials.DELETE path) +-- ============================================================================ + +test.register_coroutine_test( + "deleteCredential (pre-configured, PASS): removes credential and emits success with indices", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearPINCodeResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 2, credentialIndex = 2, credentialType = "pin" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 2, userName = "Bob", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteCredential (pre-configured, FAIL): emits failure when the lock rejects the clear", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 2, "pin" } }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 2) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearPINCodeResponse.build_test_rx(mock_device, ResponseStatus.FAIL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteAllCredentials standalone (lockCredentials.DELETE_ALL path) +-- ============================================================================ + +test.register_coroutine_test( + "deleteAllCredentials (pre-configured, PASS): clears only credentials and emits success", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearAllPINCodesResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + + -- Only credentials table is cleared; users table is untouched (no lockUsers.users event) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllCredentials (pre-configured, FAIL): emits failure and leaves tables unchanged", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteAllCredentials", args = {} }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearAllPINCodesResponse.build_test_rx(mock_device, ResponseStatus.FAIL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteAllUsers (lockUsers.DELETE_ALL path — clears both tables) +-- ============================================================================ + +test.register_coroutine_test( + "deleteAllUsers (pre-configured, PASS): clears both tables and emits success for both capabilities", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteAllUsers", args = {} }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearAllPINCodesResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllUsers (pre-configured, FAIL): emits failure for both capabilities when the lock rejects", + function() + setup_state() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteAllUsers", args = {} }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearAllPINCodesResponse.build_test_rx(mock_device, ResponseStatus.FAIL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_lock_programming_events.lua b/drivers/SmartThings/zigbee-lock/src/test/test_lock_programming_events.lua new file mode 100644 index 0000000000..3c06afef66 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_lock_programming_events.lua @@ -0,0 +1,574 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Tests for the ProgrammingEventNotification handler in lock_handlers/zigbee_responses.lua. +-- +-- Cases covered: +-- • PIN_CODE_ADDED received while NOT busy (manual addition at the lock) +-- • PIN_CODE_DELETED received while NOT busy (manual deletion at the lock) +-- • PIN_CODE_CHANGED received while NOT busy (manual update — not currently handled) +-- • PIN_CODE_ADDED while addCredential in flight with matching user (failsafe path) +-- • PIN_CODE_CHANGED while updateCredential in flight with matching user (failsafe path) +-- • PIN_CODE_DELETED while deleteCredential in flight with matching user (failsafe path) +-- • PIN_CODE_ADDED while busy with DIFFERENT user (processed as manual event) +-- • PIN_CODE_DELETED while busy with DIFFERENT user (processed as manual event) +-- • PIN_CODE_ADDED received after BUSY ends (late notification from our SetPINCode; credential +-- not double-added) +-- • PIN_CODE_ADDED received after BUSY ends (both entries already exist; complete no-op) +-- • PIN_CODE_DELETED received after BUSY ends (late notification from our ClearPINCode; credential +-- already deleted) +-- • PIN_CODE_CHANGED received after BUSY ends (late notification from our SetPINCode update; +-- not handled, no effect) + +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local clusters = require "st.zigbee.zcl.clusters" +local DoorLock = clusters.DoorLock +local capabilities = require "st.capabilities" +local constants = require "lock_utils.constants" + +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType +local ProgrammingEventCode = DoorLock.types.ProgramEventCode +local SetCodeStatus = DoorLock.types.DrlkSetCodeStatus +local ResponseStatus = DoorLock.types.DrlkPassFailStatus + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) +end + +test.set_test_init_function(test_init) + +-- Build a ProgrammingEventNotification ZigBee receive message for the given event code and user ID. +local function build_programming_event(event_code, user_id) + return { + mock_device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + mock_device, + 0x00, -- program_event_source (keypad) + event_code, + user_id, + "1234", -- PIN (not used by the handler) + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, -- local_alarm_mask + "data" -- user_description + ) + } +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- NOT-BUSY CASES (manual events from the lock, no command in flight) +-- ───────────────────────────────────────────────────────────────────────────── + +test.register_coroutine_test( + "ProgrammingEventNotification PIN_CODE_ADDED while not busy syncs user and credential entries", + function() + -- Route events to the new capabilities handler, not the legacy lockCodes handler. + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + -- Users table receives the new entry + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + ) + ) + ) + -- Credentials table receives the new entry + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ProgrammingEventNotification PIN_CODE_DELETED while not busy removes user and credential entries", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + -- Set up an existing entry by processing a manual addition first. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + + -- Now delete the same entry via a manual deletion event. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_DELETED, 1)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } }) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ProgrammingEventNotification PIN_CODE_CHANGED while not busy has no effect on table state", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + -- PIN_CODE_CHANGED is not handled by the notification handler, so no events should + -- be emitted and table state should remain unchanged. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_CHANGED, 1)) + test.wait_for_events() + end +) + +-- ───────────────────────────────────────────────────────────────────────────── +-- BUSY CASES (notification arrives while one of our commands is in flight) +-- The handler must be a no-op so we do not double-process the result. +-- ───────────────────────────────────────────────────────────────────────────── + +test.register_coroutine_test( + "ProgrammingEventNotification PIN_CODE_ADDED while addCredential is in flight acts as failsafe", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + -- Simulate an addCredential command being in flight with matching user ID. + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.ADD, {}) + mock_device:set_field(constants.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, { + userIndex = 1, credentialIndex = 1, credentialType = "pin" + }, {}) + + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + -- Failsafe path: notification handled as command success, user and credential added, commandResult emitted. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ProgrammingEventNotification PIN_CODE_CHANGED while updateCredential is in flight acts as failsafe", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + -- Simulate an updateCredential command being in flight with matching user ID. + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.UPDATE, {}) + mock_device:set_field(constants.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, { + userIndex = 1, credentialIndex = 1, credentialType = "pin" + }, {}) + + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_CHANGED, 1)) + -- Failsafe path: notification handled as command success, commandResult emitted. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ProgrammingEventNotification PIN_CODE_DELETED while deleteCredential is in flight acts as failsafe", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + -- First, add a credential so we have something to delete via the failsafe path. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + + -- Simulate a deleteCredential command being in flight with matching user ID. + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.DELETE, {}) + mock_device:set_field(constants.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, { + userIndex = 1, credentialIndex = 1, credentialType = "pin" + }, {}) + + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_DELETED, 1)) + -- Failsafe path: notification handled as command success, credential and user removed, commandResult emitted. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + end +) + +-- ───────────────────────────────────────────────────────────────────────────── +-- BUSY WITH DIFFERENT USER (notification for a different user arrives while +-- a command is in flight; processed as a normal manual event) +-- ───────────────────────────────────────────────────────────────────────────── + +test.register_coroutine_test( + "ProgrammingEventNotification PIN_CODE_ADDED for different user while busy is processed as manual event", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + -- Simulate an addCredential command in flight for user 1. + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.ADD, {}) + mock_device:set_field(constants.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, { + userIndex = 1, credentialIndex = 1, credentialType = "pin" + }, {}) + + -- Notification arrives for user 2 (different user) — should be processed as manual event. + -- The manual handler assigns userIndex = next_index (1, the first available slot), not user_id (2). + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 2)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 2, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "ProgrammingEventNotification PIN_CODE_DELETED for different user while busy is processed as manual event", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + -- First, add a credential for user 2 so we have something to delete. + -- The manual handler assigns userIndex = next_index (1, the first available slot), not user_id (2). + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 2)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 2, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + ) + ) + ) + test.wait_for_events() + + -- Simulate a deleteCredential command in flight for user 1. + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.LOCK_CREDENTIALS.DELETE, {}) + mock_device:set_field(constants.DRIVER_STATE.CREDENTIAL_ARGS_IN_USE, { + userIndex = 1, credentialIndex = 1, credentialType = "pin" + }, {}) + + -- Notification arrives for user 2 (different user) — should be processed as manual deletion. + -- Delete handler emits credentials first, then users. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_DELETED, 2)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } }) + ) + ) + test.wait_for_events() + end +) + +-- ───────────────────────────────────────────────────────────────────────────── +-- POST-BUSY CASES (busy state was already cleared by the ZigBee response +-- handler before the ProgrammingEventNotification arrives; the notification was +-- sent by the lock in response to our own SetPINCode / ClearPINCode command) +-- ───────────────────────────────────────────────────────────────────────────── + +test.register_coroutine_test( + "Late PIN_CODE_ADDED after addCredential: user and credential already exist; complete no-op", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + -- Complete an addCredential flow so both the user and credentials tables already have entries. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 1, "guest", "pin", "1234" } }, + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "1234"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.SUCCESS), + }) + -- User and credential added; commandResult emitted; busy state cleared. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Late ProgrammingEventNotification PIN_CODE_ADDED for the same slot arrives after busy ends. + -- The credential is already in the table → find_entry returns it → handler exits early. + -- Both user and credential already exist, so no events are emitted. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Late PIN_CODE_ADDED when both user and credential already exist: both add_entry calls are no-ops", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + -- Populate both tables by processing a not-busy PIN_CODE_ADDED (simulates the completed command state). + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Late notification for the same slot; both add_entry calls return OCCUPIED → no events. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Late PIN_CODE_DELETED after deleteCredential: credential and user already removed; no-op", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + -- Populate both tables (user at index 1, credential at index 1). + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Run a standalone deleteCredential flow; this removes the credential but leaves the user. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "deleteCredential", + args = { 1, "pin" } }, + }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 1) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearPINCodeResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + -- Credential and user entries removed (no remaining credentials for this user). Busy state cleared. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 1, userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Late PIN_CODE_DELETED for the same slot arrives after busy ends. + -- The credential and user are already gone → therefore nothing happens. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_DELETED, 1)) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "Late PIN_CODE_CHANGED after updateCredential: not handled by notification handler, no events emitted", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + -- Seed user and credential so updateCredential has an existing entry to modify. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_ADDED, 1)) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Complete an updateCredential flow so the credential entry is updated and busy state is cleared. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "updateCredential", + args = { 1, 1, "pin", "5678" } }, + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 1, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "5678"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.SUCCESS), + }) + -- UPDATE doesn't modify the credentials table metadata, only the PIN code (not stored). + -- The response handler just emits commandResult; busy state is cleared. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "updateCredential", statusCode = "success", userIndex = 1, credentialIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Late PIN_CODE_CHANGED notification arrives after busy ends. + -- The notification handler does not handle PIN_CODE_CHANGED → no events emitted. + test.socket.zigbee:__queue_receive(build_programming_event(ProgrammingEventCode.PIN_CODE_CHANGED, 1)) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_lock_tables.lua b/drivers/SmartThings/zigbee-lock/src/test/test_lock_tables.lua new file mode 100644 index 0000000000..82ecd7f964 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_lock_tables.lua @@ -0,0 +1,728 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Unit tests for lock_utils/tables.lua +-- Tests directly call table_utils functions and verify both return values +-- and emitted capability events. + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local capabilities = require "st.capabilities" +local st_utils = require "st.utils" +local table_utils = require "lock_utils.tables" +local constants = require "lock_utils.constants" + +-- --------------------------------------------------------------------------- +-- Shared mock device +-- --------------------------------------------------------------------------- + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), +}) +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +-- Seed the users table with `entries` and consume the resulting emit_events. +-- After this call the state cache has those entries and the socket is clean. +local function seed_users(entries) + for _, entry in ipairs(entries) do + -- Build the expected post-insert table up to this entry. + local expected = {} + for _, e in ipairs(entries) do + table.insert(expected, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(expected, { visibility = { displayed = false } })) + ) + local result = table_utils.add_entry(mock_device, "users", entry) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "seed_users: add_entry failed for entry userIndex=" .. tostring(entry.userIndex)) + end + test.wait_for_events() +end + +-- Seed the credentials table with `entries` and consume the resulting emit_events. +local function seed_credentials(entries) + for _, entry in ipairs(entries) do + local expected = {} + for _, e in ipairs(entries) do + table.insert(expected, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials(expected, { visibility = { displayed = false } })) + ) + local result = table_utils.add_entry(mock_device, "credentials", entry) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "seed_credentials: add_entry failed for entry credentialIndex=" .. tostring(entry.credentialIndex)) + end + test.wait_for_events() +end + +-- =========================================================================== +-- add_entry +-- =========================================================================== + +test.register_coroutine_test( + "add_entry: adds a new user entry and emits the updated table", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({ entry }, { visibility = { displayed = false } })) + ) + + local result = table_utils.add_entry(mock_device, "users", entry) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "add_entry: returns OCCUPIED when an entry with the same userIndex already exists", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ entry }) + + -- Attempt to add a different entry with the same userIndex (match_key) + local duplicate = { userIndex = 1, userType = "unrestricted", userName = "Bob" } + local result = table_utils.add_entry(mock_device, "users", duplicate) + assert(result == constants.COMMAND_RESULT.OCCUPIED, + "Expected OCCUPIED, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: returns RESOURCE_EXHAUSTED when table is at max capacity", + function() + local entries = {} + for i = 1, 20 do + entries[i] = { userIndex = i, userType = "guest", userName = "User" .. i } + end + seed_users(entries) + + local overflow = { userIndex = 21, userType = "guest", userName = "Overflow" } + local result = table_utils.add_entry(mock_device, "users", overflow) + assert(result == constants.COMMAND_RESULT.RESOURCE_EXHAUSTED, + "Expected RESOURCE_EXHAUSTED, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: returns FAILURE when a required key is missing", + function() + -- userType is required for users table + local incomplete = { userIndex = 1 } + local result = table_utils.add_entry(mock_device, "users", incomplete) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: returns FAILURE for an unknown table name", + function() + local result = table_utils.add_entry(mock_device, "nonexistent_table", + { userIndex = 1, userType = "guest" }) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "add_entry: adds a credential entry and emits updated credentials table", + function() + local entry = { userIndex = 1, credentialIndex = 1, credentialType = "pin" } + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({ entry }, { visibility = { displayed = false } })) + ) + + local result = table_utils.add_entry(mock_device, "credentials", entry) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +-- =========================================================================== +-- update_entry +-- =========================================================================== + +test.register_coroutine_test( + "update_entry: updates an existing user entry and emits updated table", + function() + local original = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ original }) + + local expected_after_update = { + { userIndex = 1, userType = "adminMember", userName = "Alice_Updated" } + } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(expected_after_update, { visibility = { displayed = false } })) + ) + + local result = table_utils.update_entry(mock_device, "users", 1, + { userName = "Alice_Updated", userType = "adminMember" }) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "update_entry: returns FAILURE when no entry matches the match_key value", + function() + local result = table_utils.update_entry(mock_device, "users", 99, + { userName = "Ghost" }) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "update_entry: only updates the specified fields, leaves others intact", + function() + local entry1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local entry2 = { userIndex = 2, userType = "guest", userName = "Bob" } + seed_users({ entry1, entry2 }) + + -- Update only userName of entry 2; userType should remain "guest" + local expected = { + { userIndex = 1, userType = "guest", userName = "Alice" }, + { userIndex = 2, userType = "guest", userName = "Bob_Renamed" }, + } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(expected, { visibility = { displayed = false } })) + ) + + local result = table_utils.update_entry(mock_device, "users", 2, { userName = "Bob_Renamed" }) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "update_entry: returns FAILURE for an unknown table name", + function() + local result = table_utils.update_entry(mock_device, "bad_table", 1, { userName = "X" }) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- delete_entry +-- =========================================================================== + +test.register_coroutine_test( + "delete_entry: deletes an existing entry and returns COMMAND_RESULT.SUCCESS", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ entry }) + + -- After deletion the table is empty + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_entry(mock_device, "users", 1) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "delete_entry: returns FAILURE when no entry matches the match_key value", + function() + local result = table_utils.delete_entry(mock_device, "users", 99) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "delete_entry: remaining entries are intact after a deletion", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + local e3 = { userIndex = 3, userType = "guest", userName = "Carol" } + seed_users({ e1, e2, e3 }) + + -- Delete the middle entry; expect e1 and e3 remain + local expected_remaining = { + { userIndex = 1, userType = "guest", userName = "Alice" }, + { userIndex = 3, userType = "guest", userName = "Carol" }, + } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(expected_remaining, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_entry(mock_device, "users", 2) + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "delete_entry: returns FAILURE for an unknown table name", + function() + local result = table_utils.delete_entry(mock_device, "bad_table", 1) + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- delete_all_entries +-- =========================================================================== + +test.register_coroutine_test( + "delete_all_entries: emits an empty users table and returns SUCCESS", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + seed_users({ e1, e2 }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_all_entries(mock_device, "users") + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "delete_all_entries: returns SUCCESS even when the table is already empty", + function() + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_all_entries(mock_device, "users") + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +test.register_coroutine_test( + "delete_all_entries: returns FAILURE for an unknown table name", + function() + local result = table_utils.delete_all_entries(mock_device, "bad_table") + assert(result == constants.COMMAND_RESULT.FAILURE, + "Expected FAILURE, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "delete_all_entries: emits an empty credentials table and returns SUCCESS", + function() + local cred = { userIndex = 1, credentialIndex = 1, credentialType = "pin" } + seed_credentials({ cred }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + + local result = table_utils.delete_all_entries(mock_device, "credentials") + assert(result == constants.COMMAND_RESULT.SUCCESS, + "Expected SUCCESS, got: " .. tostring(result)) + + test.wait_for_events() + end +) + +-- =========================================================================== +-- find_entry +-- =========================================================================== + +test.register_coroutine_test( + "find_entry: returns the matching entry when it exists", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + seed_users({ e1, e2 }) + + local result = table_utils.find_entry(mock_device, "users", 2) + assert(type(result) == "table", + "Expected a table entry, got: " .. tostring(result)) + assert(result.userIndex == 2, + "Expected userIndex == 2, got: " .. tostring(result.userIndex)) + assert(result.userName == "Bob", + "Expected userName == 'Bob', got: " .. tostring(result.userName)) + end +) + +test.register_coroutine_test( + "find_entry: returns nil when no entry matches the value", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ e1 }) + + local result = table_utils.find_entry(mock_device, "users", 99) + assert(result == nil, + "Expected nil for missing entry, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "find_entry: finds a credential entry by credentialIndex", + function() + local cred = { userIndex = 1, credentialIndex = 5, credentialType = "pin" } + seed_credentials({ cred }) + + local result = table_utils.find_entry(mock_device, "credentials", 5) + assert(type(result) == "table", + "Expected a table entry, got: " .. tostring(result)) + assert(result.credentialIndex == 5, + "Expected credentialIndex == 5, got: " .. tostring(result.credentialIndex)) + end +) + +-- =========================================================================== +-- find_entry_by +-- =========================================================================== + +test.register_coroutine_test( + "find_entry_by: returns the entry matching an arbitrary key", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "adminMember", userName = "Bob" } + seed_users({ e1, e2 }) + + local result = table_utils.find_entry_by(mock_device, "users", "userName", "Bob") + assert(type(result) == "table", + "Expected a table entry, got: " .. tostring(result)) + assert(result.userIndex == 2, + "Expected userIndex == 2, got: " .. tostring(result.userIndex)) + end +) + +test.register_coroutine_test( + "find_entry_by: returns nil when no entry matches", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ e1 }) + + local result = table_utils.find_entry_by(mock_device, "users", "userName", "Nobody") + assert(result == nil, + "Expected nil for unmatched key/value, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "find_entry_by: returns the first matching entry when multiple entries share the same value", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + local e3 = { userIndex = 3, userType = "guest", userName = "Carol" } + seed_users({ e1, e2, e3 }) + + local result = table_utils.find_entry_by(mock_device, "users", "userType", "guest") + assert(type(result) == "table", + "Expected a table entry, got: " .. tostring(result)) + assert(result.userIndex == 1, + "Expected first match userIndex == 1, got: " .. tostring(result.userIndex)) + end +) + +-- =========================================================================== +-- next_index +-- =========================================================================== + +test.register_coroutine_test( + "next_index: returns 1 when the table is empty", + function() + local result = table_utils.next_index(mock_device, "users") + assert(result == 1, + "Expected 1 for empty table, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "next_index: returns the next sequential index after a contiguous range", + function() + local entries = {} + for i = 1, 3 do + entries[i] = { userIndex = i, userType = "guest", userName = "User" .. i } + end + seed_users(entries) + + local result = table_utils.next_index(mock_device, "users") + assert(result == 4, + "Expected 4 after indices 1-3, got: " .. tostring(result)) + end +) + +-- this should not happen during normal operation. +test.register_coroutine_test( + "next_index: returns the lowest gap when indices are non-contiguous", + function() + -- Insert entries at indices 1 and 3, leaving a gap at 2 + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e3 = { userIndex = 3, userType = "guest", userName = "Carol" } + seed_users({ e1, e3 }) + + local result = table_utils.next_index(mock_device, "users") + assert(result == 2, + "Expected 2 as the lowest gap, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- get_max_entries +-- =========================================================================== + +test.register_coroutine_test( + "get_max_entries: returns the default of 20 when the attribute is not set", + function() + local result = table_utils.get_max_entries(mock_device, "users") + assert(result == 20, + "Expected default max of 20, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "get_max_entries: returns the default of 20 for the credentials table when the attribute is not set", + function() + local result = table_utils.get_max_entries(mock_device, "credentials") + assert(result == 20, + "Expected default max of 20, got: " .. tostring(result)) + end +) + +-- =========================================================================== +-- Persistence — mutations write to the persistent store +-- =========================================================================== + +test.register_coroutine_test( + "persist: add_entry writes the new users table to the persistent store immediately", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({ entry }, { visibility = { displayed = false } })) + ) + table_utils.add_entry(mock_device, "users", entry) + test.wait_for_events() + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedUsers")) + assert(type(persisted) == "table" and #persisted == 1, + "Expected 1 persisted user, got: " .. tostring(persisted and #persisted)) + assert(persisted[1].userIndex == 1 and persisted[1].userName == "Alice", + "Persisted user data does not match the added entry") + end +) + +test.register_coroutine_test( + "persist: update_entry writes the updated users table to the persistent store immediately", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ entry }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userType = "adminMember", userName = "Alice" } }, + { visibility = { displayed = false } })) + ) + table_utils.update_entry(mock_device, "users", 1, { userType = "adminMember" }) + test.wait_for_events() + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedUsers")) + assert(type(persisted) == "table" and #persisted == 1, + "Expected 1 persisted user after update") + assert(persisted[1].userType == "adminMember", + "Persisted user type was not updated, got: " .. tostring(persisted[1].userType)) + end +) + +test.register_coroutine_test( + "persist: delete_entry removes the entry from the persistent store immediately", + function() + local e1 = { userIndex = 1, userType = "guest", userName = "Alice" } + local e2 = { userIndex = 2, userType = "guest", userName = "Bob" } + seed_users({ e1, e2 }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({ e2 }, { visibility = { displayed = false } })) + ) + table_utils.delete_entry(mock_device, "users", 1) + test.wait_for_events() + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedUsers")) + assert(type(persisted) == "table" and #persisted == 1, + "Expected 1 persisted user after delete, got: " .. tostring(persisted and #persisted)) + assert(persisted[1].userIndex == 2, + "Expected remaining user to have userIndex == 2, got: " .. tostring(persisted[1].userIndex)) + end +) + +test.register_coroutine_test( + "persist: delete_all_entries writes an empty table to the persistent store", + function() + local entry = { userIndex = 1, userType = "guest", userName = "Alice" } + seed_users({ entry }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + table_utils.delete_all_entries(mock_device, "users") + test.wait_for_events() + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedUsers")) + assert(type(persisted) == "table" and #persisted == 0, + "Expected empty persistent store after delete_all, got: " .. tostring(persisted and #persisted)) + end +) + +test.register_coroutine_test( + "persist: add_entry writes the new credentials table to the persistent store immediately", + function() + local cred = { userIndex = 1, credentialIndex = 1, credentialType = "pin" } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({ cred }, { visibility = { displayed = false } })) + ) + table_utils.add_entry(mock_device, "credentials", cred) + test.wait_for_events() + + local persisted = st_utils.deep_copy(mock_device:get_field("persistedCredentials")) + assert(type(persisted) == "table" and #persisted == 1, + "Expected 1 persisted credential, got: " .. tostring(persisted and #persisted)) + assert(persisted[1].credentialIndex == 1, + "Persisted credential index does not match") + end +) + +-- =========================================================================== +-- Persistence — get_state falls back to the persistent store +-- =========================================================================== + +test.register_coroutine_test( + "persist: get_state returns data from the persistent store when capability state cache is absent", + function() + -- The device was loaded with a pre-seeded persistent field (set in test_init + -- below), but no capability event has been emitted for users, so get_latest_state + -- returns nil. get_state must fall back to the persistent store. + local state = table_utils.get_state(mock_device, "users") + assert(type(state) == "table" and #state == 1, + "Expected 1 user from persistent-store fallback, got: " .. tostring(state and #state)) + assert(state[1].userIndex == 1 and state[1].userName == "Alice", + "Fallback data does not match pre-seeded persistent entry") + end, + { + test_init = function() + -- Pre-seed persistent store BEFORE add_test_device so that wrapped_init + -- copies the field into the device's persistent_store on startup. + mock_device:set_field( + "persistedUsers", + { { userIndex = 1, userType = "guest", userName = "Alice" } }, + { persist = true } + ) + test.mock_device.add_test_device(mock_device) + end, + } +) + +-- =========================================================================== +-- Persistence — restore_from_persistent_store re-emits stored tables +-- =========================================================================== + +test.register_coroutine_test( + "persist: restore_from_persistent_store emits users capability event for stored data", + function() + local user = { userIndex = 1, userType = "guest", userName = "Alice" } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({ user }, { visibility = { displayed = false } })) + ) + table_utils.restore_from_persistent_store(mock_device) + test.wait_for_events() + end, + { + test_init = function() + mock_device:set_field( + "persistedUsers", + { { userIndex = 1, userType = "guest", userName = "Alice" } }, + { persist = true } + ) + test.mock_device.add_test_device(mock_device) + end, + } +) + +test.register_coroutine_test( + "persist: restore_from_persistent_store emits credentials capability event for stored data", + function() + local cred = { userIndex = 1, credentialIndex = 1, credentialType = "pin" } + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({ cred }, { visibility = { displayed = false } })) + ) + table_utils.restore_from_persistent_store(mock_device) + test.wait_for_events() + end, + { + test_init = function() + mock_device:set_field( + "persistedCredentials", + { { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }, + { persist = true } + ) + test.mock_device.add_test_device(mock_device) + end, + } +) + +test.register_coroutine_test( + "persist: restore_from_persistent_store is a no-op when the persistent store is empty", + function() + -- No capability events expected when there is nothing in the persistent store. + table_utils.restore_from_persistent_store(mock_device) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_lock_users_commands.lua b/drivers/SmartThings/zigbee-lock/src/test/test_lock_users_commands.lua new file mode 100644 index 0000000000..2f68f0cfb6 --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_lock_users_commands.lua @@ -0,0 +1,654 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Integration tests for the lockUsers capability commands: +-- addUser, updateUser, deleteUser, deleteAllUsers + +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local DoorLock = clusters.DoorLock +local table_utils = require "lock_utils.tables" +local constants = require "lock_utils.constants" + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), +}) +zigbee_test_utils.prepare_zigbee_env_info() + +-- ── helpers ──────────────────────────────────────────────────────────────── + +local function test_init() + test.mock_device.add_test_device(mock_device) +end +test.set_test_init_function(test_init) + +-- Directly insert users into the device state via table_utils (mirrors test_lock_tables.lua). +-- Consumes the resulting capability events so the socket queue stays clean. +local function seed_users(entries) + for _, entry in ipairs(entries) do + local so_far = {} + for _, e in ipairs(entries) do + table.insert(so_far, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users(so_far, { visibility = { displayed = false } })) + ) + assert(table_utils.add_entry(mock_device, "users", entry) == constants.COMMAND_RESULT.SUCCESS, + "seed_users: add_entry failed for userIndex=" .. tostring(entry.userIndex)) + end + test.wait_for_events() +end + +-- Directly insert credentials into the device state. +local function seed_credentials(entries) + for _, entry in ipairs(entries) do + local so_far = {} + for _, e in ipairs(entries) do + table.insert(so_far, e) + if e == entry then break end + end + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials(so_far, { visibility = { displayed = false } })) + ) + assert(table_utils.add_entry(mock_device, "credentials", entry) == constants.COMMAND_RESULT.SUCCESS, + "seed_credentials: add_entry failed for credentialIndex=" .. tostring(entry.credentialIndex)) + end + test.wait_for_events() +end + +-- Set totalUsersSupported on the mock device and consume the resulting event. +local function set_total_users_supported(n) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.totalUsersSupported(n, { visibility = { displayed = false } })) + ) + mock_device:emit_event(capabilities.lockUsers.totalUsersSupported(n, { visibility = { displayed = false } })) + test.wait_for_events() +end + +-- ============================================================================ +-- addUser +-- ============================================================================ + +test.register_coroutine_test( + "addUser: assigns userIndex 1 for the first user and emits a success commandResult", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Alice", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Alice", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addUser: assigns the next sequential userIndex when users already exist", + function() + seed_users({ { userIndex = 1, userName = "Alice", userType = "guest" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Bob", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addUser: fills a gap left by a deleted user rather than appending beyond max", + function() + -- Seed indices 1 and 3; index 2 is the expected gap to fill + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Bob", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "addUser: returns resourceExhausted when totalUsersSupported has been reached", + function() + set_total_users_supported(2) + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Carol", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "resourceExhausted" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- updateUser +-- ============================================================================ + +test.register_coroutine_test( + "updateUser: updates an existing user and emits a success commandResult with userIndex", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 1, "AliceUpdated", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "AliceUpdated", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateUser: adds user and returns success when the target userIndex does not exist", + function() + -- empty table, add user + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 99, "Ghost", "guest" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 99, userName = "Ghost", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 99 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "updateUser: can change a user's type as well as name", + function() + seed_users({ { userIndex = 1, userName = "Alice", userType = "guest" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "updateUser", args = { 1, "Alice", "adminMember" } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Alice", userType = "adminMember" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "updateUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteUser (no associated credential — pure local delete) +-- ============================================================================ + +test.register_coroutine_test( + "deleteUser: removes a user with no credential and emits a success commandResult with userIndex", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 1 } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 2, userName = "Bob", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 1 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteUser: returns failure when the target userIndex is not in the users table", + function() + -- empty table + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 99 } }, + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- deleteAllUsers (injects deleteAllCredentials → ClearAllPINCodes zigbee flow) +-- ============================================================================ + +test.register_coroutine_test( + "deleteAllUsers: sends ClearAllPINCodes and emits success for both users and credentials on PASS", + function() + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + }) + seed_credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteAllUsers", args = {} }, + }) + + -- deleteAllUsers injects deleteAllCredentials, which sends ClearAllPINCodes + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + local ResponseStatus = DoorLock.types.DrlkPassFailStatus + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearAllPINCodesResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } })) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "success" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "deleteAllUsers: emits failure for both users and credentials when the lock returns FAIL", + function() + seed_users({ { userIndex = 1, userName = "Alice", userType = "guest" } }) + seed_credentials({ { userIndex = 1, credentialIndex = 1, credentialType = "pin" } }) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteAllUsers", args = {} }, + }) + + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearAllPINCodes(mock_device) }) + test.wait_for_events() + + local ResponseStatus = DoorLock.types.DrlkPassFailStatus + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearAllPINCodesResponse.build_test_rx(mock_device, ResponseStatus.FAIL), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteAllUsers", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteAllCredentials", statusCode = "failure" }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- State-consistency: add → delete → re-add (indices 1, 2, 3 lifecycle) +-- ============================================================================ +-- +-- These tests verify that the users and credentials tables stay in sync +-- through a full lifecycle: populate three slots, remove the middle one, +-- then re-add a user and credential into the freed slot. The goal is to +-- confirm there is no stale index state that would cause duplicate entries, +-- wrong slot assignment, or mismatched user↔credential links. + +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType +local SetCodeStatus = DoorLock.types.DrlkSetCodeStatus +local ResponseStatus = DoorLock.types.DrlkPassFailStatus + +test.register_coroutine_test( + "State-consistency: add users 1-3, deleteUser 2 (no credential), re-add user reclaims index 2", + function() + -- Populate three user slots directly (no credentials, so deleteUser will take + -- the no-ZigBee path and delete the user entry locally). + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }) + + -- Delete user at index 2; no credential is linked so this is a pure local delete. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 2 } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Re-add a user. next_index sees occupied = {1, 3}, so it assigns index 2. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Dave", "guest" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + { userIndex = 2, userName = "Dave", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "State-consistency: add users+credentials 1-3, deleteUser 2 (with credential), re-add user+credential reclaims index 2 cleanly", + function() + -- Populate three user and credential slots. + seed_users({ + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 2, userName = "Bob", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }) + seed_credentials({ + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, + }) + + -- Delete user at index 2. The handler finds a linked credential and injects + -- deleteCredential, which sends ClearPINCode to the lock. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "deleteUser", args = { 2 } }, + }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.SendPINOverTheAir:write(mock_device, true) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.server.commands.ClearPINCode(mock_device, 2) }) + test.wait_for_events() + + -- Lock acknowledges the deletion. + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.ClearPINCodeResponse.build_test_rx(mock_device, ResponseStatus.PASS), + }) + -- clear_pin_code_response (LOCK_USERS.DELETE path): deletes credentials first, then user. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "deleteUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "deleteCredential", statusCode = "success", credentialIndex = 2, userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Re-add a user. next_index sees occupied = {1, 3} and assigns index 2. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockUsers.ID, command = "addUser", args = { "Dave", "guest" } }, + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { + { userIndex = 1, userName = "Alice", userType = "guest" }, + { userIndex = 3, userName = "Carol", userType = "guest" }, + { userIndex = 2, userName = "Dave", userType = "guest" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.commandResult( + { commandName = "addUser", statusCode = "success", userIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + + -- Re-add a credential for the new user at slot 2. The old credential at + -- credentialIndex 2 was cleanly removed, so add_entry must succeed without + -- returning OCCUPIED or any stale-state error. + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = capabilities.lockCredentials.ID, command = "addCredential", + args = { 2, "guest", "pin", "9999" } }, + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.SetPINCode(mock_device, + 2, DoorLockUserStatus.OCCUPIED_ENABLED, DoorLockUserType.UNRESTRICTED, "9999"), + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.SetPINCodeResponse.build_test_rx(mock_device, SetCodeStatus.SUCCESS), + }) + -- Credential is freshly added at index 2 alongside the retained entries at 1 and 3. + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { + { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, + { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, + { userIndex = 2, credentialIndex = 2, credentialType = "pin", credentialName = "Guest 2" }, + }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.commandResult( + { commandName = "addCredential", statusCode = "success", userIndex = 2, credentialIndex = 2 }, + { state_change = true, visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua deleted file mode 100644 index d4ee3f770f..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_code_migration.lua +++ /dev/null @@ -1,248 +0,0 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - --- Mock out globals -local test = require "integration_test" -local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local t_utils = require "integration_test.utils" - -local clusters = require "st.zigbee.zcl.clusters" -local PowerConfiguration = clusters.PowerConfiguration -local DoorLock = clusters.DoorLock -local Alarm = clusters.Alarms - -local json = require "st.json" - -local mock_datastore = require "integration_test.mock_env_datastore" - -local mock_device = test.mock_device.build_test_zigbee_device( - { - profile = t_utils.get_profile_definition("base-lock.yml"), - data = { - lockCodes = json.encode({ - ["1"] = "Zach", - ["2"] = "Steven" - }) - } - } -) - -local mock_device_no_data = test.mock_device.build_test_zigbee_device( - { - profile = t_utils.get_profile_definition("base-lock.yml"), - data = {} - } -) -zigbee_test_utils.prepare_zigbee_env_info() -local function test_init()end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "Device added data lock codes population", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device added without data should function", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_no_data) }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, DoorLock.attributes.LockState:read(mock_device_no_data) }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, Alarm.attributes.AlarmCount:read(mock_device_no_data) }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", nil) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", nil) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", nil) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device init after added shouldn't change the datastores", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device init with new data should populate fields", - function() - test.mock_device.add_test_device(mock_device_no_data) - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "added" }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device_no_data) }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, DoorLock.attributes.LockState:read(mock_device_no_data) }) - test.socket.zigbee:__expect_send({ mock_device_no_data.id, Alarm.attributes.AlarmCount:read(mock_device_no_data) }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "lockCodes", nil) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "__state_cache", {}) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", nil) - test.socket.device_lifecycle():__queue_receive(mock_device_no_data:generate_info_changed( - { - data = { - lockCodes = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) - } - } - )) - test.wait_for_events() - test.socket.device_lifecycle:__queue_receive({ mock_device_no_data.id, "init" }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "lockCodes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device_no_data.id, "migrationComplete", true) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "Device added data lock codes population, device response produces no events", - function() - test.mock_device.add_test_device(mock_device) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) - test.wait_for_events() - -- Validate lockCodes field - mock_datastore.__assert_device_store_contains(mock_device.id, "lockCodes", { ["1"] = "Zach", ["2"] = "Steven" }) - -- Validate state cache - mock_datastore.__assert_device_store_contains(mock_device.id, "__state_cache", - { - main = { - lockCodes = { - lockCodes = {value = json.encode({ ["1"] = "Zach", ["2"] = "Steven" }) } - } - } - } - ) - -- Validate migration complete flag - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationComplete", true) - test.wait_for_events() - - -- run do_configure step after added and verify no refresh all codes - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - - test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - PowerConfiguration.ID) }) - test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, - 600, - 21600, - 1) }) - test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - DoorLock.ID) }) - test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:configure_reporting(mock_device, - 0, - 3600, - 0) }) - test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - Alarm.ID) }) - test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:configure_reporting(mock_device, - 0, - 21600, - 0) }) - - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - -- Validate migration reload skipped datastore - test.wait_for_events() - mock_datastore.__assert_device_store_contains(mock_device.id, "migrationReloadSkipped", true) - -- Verify the timer doesn't fire as it wasn't created - test.mock_time.advance_time(4) - test.wait_for_events() - end, - { - min_api_version = 17 - } -) - - -test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua similarity index 99% rename from drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua rename to drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua index 12f8331253..c3b316f486 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua @@ -18,6 +18,7 @@ local DoorLockUserStatus = DoorLock.types.DrlkUserStatus local DoorLockUserType = DoorLock.types.DrlkUserType local ProgrammingEventCode = DoorLock.types.ProgramEventCode +local consts = require "lock_utils.constants" local json = require "dkjson" local mock_device = test.mock_device.build_test_zigbee_device( @@ -25,7 +26,8 @@ local mock_device = test.mock_device.build_test_zigbee_device( ) zigbee_test_utils.prepare_zigbee_env_info() local function test_init() - test.mock_device.add_test_device(mock_device)end + test.mock_device.add_test_device(mock_device) +end test.set_test_init_function(test_init) @@ -42,6 +44,8 @@ end test.register_coroutine_test( "Configure should configure all necessary attributes and begin reading codes", function() + mock_device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, false, { persist = true }) + test.wait_for_events() test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") test.wait_for_events() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_responses.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_responses.lua new file mode 100644 index 0000000000..e561a4867a --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_responses.lua @@ -0,0 +1,566 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 +-- +-- Additional tests for lock_handlers/zigbee_responses.lua to improve coverage. +-- Covers: +-- • get_pin_code_response: sync codes from lock flow +-- • programming_event_notification: Yale/ASSA ABLOY user_id >= 256 shift +-- • operating_event_notification: keypad source with lockUsers capability +-- • operating_event_notification: schedule events with non-keypad source (auto method) +-- • alarm: alarm codes 0 and 1 +-- • lock_state: attribute handler with delay logic +-- • max_pin_code_length / min_pin_code_length: attribute handlers +-- • number_of_pin_users_supported: attribute handler with profile migration + +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local DoorLock = clusters.DoorLock +local Alarms = clusters.Alarms +local constants = require "lock_utils.constants" +local table_utils = require "lock_utils.tables" + +local DoorLockUserStatus = DoorLock.types.DrlkUserStatus +local DoorLockUserType = DoorLock.types.DrlkUserType +local OperationEventCode = DoorLock.types.OperationEventCode +local OperationEventSource = DoorLock.types.DrlkOperationEventSource +local ProgrammingEventCode = DoorLock.types.ProgramEventCode +local LockState = DoorLock.attributes.LockState + +local mock_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), +}) + +local mock_device_yale = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Yale", + model = "YRD256", + server_clusters = { DoorLock.ID }, + }, + }, +}) + +local mock_device_no_lock_codes = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("lock-without-codes.yml"), +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_device_yale) + test.mock_device.add_test_device(mock_device_no_lock_codes) +end + +test.set_test_init_function(test_init) + +-- Helper: build OperatingEventNotification +local function build_operating_event(device, event_code, event_source, user_id) + return { + device.id, + DoorLock.client.commands.OperatingEventNotification.build_test_rx( + device, + event_source, + event_code, + user_id or 0, + "1234", + 0x0000, + "data" + ), + } +end + +-- Helper: build ProgrammingEventNotification +local function build_programming_event(device, event_code, user_id) + return { + device.id, + DoorLock.client.commands.ProgrammingEventNotification.build_test_rx( + device, + 0x00, + event_code, + user_id, + "1234", + DoorLockUserType.UNRESTRICTED, + DoorLockUserStatus.OCCUPIED_ENABLED, + 0x0000, + "data" + ), + } +end + +-- ============================================================================ +-- get_pin_code_response: sync codes from lock +-- ============================================================================ + +test.register_coroutine_test( + "get_pin_code_response: syncs codes from lock and requests next code", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + -- Set up the sync state + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.SYNC.CODES_FROM_LOCK, {}) + mock_device:set_field(constants.SYNC.CODE_INDEX, 1, {}) + + -- Receive GetPINCodeResponse for user_id 1 + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 1, -- user_id + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" -- PIN code + ), + }) + + -- Should add user entry + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + -- Should add credential entry + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + -- Should request next code + test.socket.zigbee:__expect_send({ + mock_device.id, + DoorLock.server.commands.GetPINCode(mock_device, 2), + }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "get_pin_code_response: completes sync when max entries reached", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + -- Set up the sync state at the last code index + -- get_max_entries defaults to 20 when attribute is missing, so use user_id 20 + mock_device:set_field(constants.DRIVER_STATE.BUSY, os.time(), {}) + mock_device:set_field(constants.DRIVER_STATE.COMMAND_IN_PROGRESS, constants.SYNC.CODES_FROM_LOCK, {}) + mock_device:set_field(constants.SYNC.CODE_INDEX, 20, {}) + + -- Receive GetPINCodeResponse for user_id 20 (at default max) + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.client.commands.GetPINCodeResponse.build_test_rx( + mock_device, + 20, + DoorLockUserStatus.OCCUPIED_ENABLED, + DoorLockUserType.UNRESTRICTED, + "1234" + ), + }) + + -- Should add entries + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 20, userName = "Guest 20", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 20, credentialIndex = 20, credentialType = "pin", credentialName = "Guest 20" } }, + { visibility = { displayed = false } } + )) + ) + -- Should NOT request next code (sync complete) + test.wait_for_events() + + -- Verify sync state is cleared (clear_busy_state sets BUSY to false, not nil) + assert(mock_device:get_field(constants.SYNC.CODE_INDEX) == nil) + assert(mock_device:get_field(constants.DRIVER_STATE.BUSY) == false) + end +) + +-- ============================================================================ +-- programming_event_notification: Yale user_id shift +-- ============================================================================ + +test.register_coroutine_test( + "programming_event_notification: Yale device shifts user_id >= 256", + function() + mock_device_yale:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + -- Send a PIN_CODE_ADDED with user_id 256 (0x100), which should shift to 1 + test.socket.zigbee:__queue_receive( + build_programming_event(mock_device_yale, ProgrammingEventCode.PIN_CODE_ADDED, 256) + ) + + -- After shifting 256 >> 8 = 1, should add entries for user_id 1 + test.socket.capability:__expect_send( + mock_device_yale:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "Guest 1", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + test.socket.capability:__expect_send( + mock_device_yale:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "Guest 1" } }, + { visibility = { displayed = false } } + )) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- operating_event_notification: keypad with lockUsers (user info lookup) +-- ============================================================================ + +test.register_coroutine_test( + "operating_event_notification: keypad unlock includes user info when user and credential exist", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + -- First, add a user entry (using valid userType "guest") + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.users( + { { userIndex = 1, userName = "John Doe", userType = "guest" } }, + { visibility = { displayed = false } } + )) + ) + table_utils.add_entry(mock_device, "users", { + userIndex = 1, + userName = "John Doe", + userType = "guest", + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.credentials( + { { userIndex = 1, credentialIndex = 1, credentialType = "pin", credentialName = "John's PIN" } }, + { visibility = { displayed = false } } + )) + ) + table_utils.add_entry(mock_device, "credentials", { + userIndex = 1, + credentialIndex = 1, + credentialType = "pin", + credentialName = "John's PIN", + }) + test.wait_for_events() + + -- Send unlock event from keypad with user_id 1 + test.socket.zigbee:__queue_receive( + build_operating_event(mock_device, OperationEventCode.UNLOCK, OperationEventSource.KEYPAD, 1) + ) + + -- Should emit unlocked with user info + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ + data = { + method = "keypad", + userIndex = 1, + userName = "John Doe", + userType = "guest", + }, + }) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "operating_event_notification: keypad unlock with unknown user uses default name", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + -- Send unlock event from keypad with user_id 99 (no matching entry) + test.socket.zigbee:__queue_receive( + build_operating_event(mock_device, OperationEventCode.UNLOCK, OperationEventSource.KEYPAD, 99) + ) + + -- Should emit unlocked with default user name + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ + data = { + method = "keypad", + userIndex = 99, + userName = "Guest 99", + }, + }) + ) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- operating_event_notification: schedule events with "auto" method +-- ============================================================================ + +test.register_coroutine_test( + "operating_event_notification: SCHEDULE_LOCK with RF source uses auto method", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + test.socket.zigbee:__queue_receive( + build_operating_event(mock_device, OperationEventCode.SCHEDULE_LOCK, OperationEventSource.RF, 0) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked({ data = { method = "auto" } }) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "operating_event_notification: SCHEDULE_UNLOCK with MANUAL source uses auto method", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + test.socket.zigbee:__queue_receive( + build_operating_event(mock_device, OperationEventCode.SCHEDULE_UNLOCK, OperationEventSource.MANUAL, 0) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.unlocked({ data = { method = "auto" } }) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "operating_event_notification: AUTO_LOCK with RF source uses auto method", + function() + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + test.socket.zigbee:__queue_receive( + build_operating_event(mock_device, OperationEventCode.AUTO_LOCK, OperationEventSource.RF, 0) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lock.lock.locked({ data = { method = "auto" } }) + ) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- alarm: alarm handler +-- ============================================================================ + +test.register_coroutine_test( + "alarm: alarm code 0 emits lock.unknown", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + Alarms.client.commands.Alarm.build_test_rx(mock_device, 0, DoorLock.ID), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "alarm: alarm code 1 emits lock.unknown", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + Alarms.client.commands.Alarm.build_test_rx(mock_device, 1, DoorLock.ID), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "alarm: unrecognized alarm code does not emit event", + function() + -- Alarm code 16 (low battery) is not in ALARM_REPORT map + test.socket.zigbee:__queue_receive({ + mock_device.id, + Alarms.client.commands.Alarm.build_test_rx(mock_device, 16, DoorLock.ID), + }) + + -- No event should be emitted + test.wait_for_events() + end +) + +-- ============================================================================ +-- lock_state: attribute handler +-- ============================================================================ + +test.register_coroutine_test( + "lock_state: LOCKED state emits locked event", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + LockState:build_test_attr_report(mock_device, LockState.LOCKED), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.locked()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "lock_state: UNLOCKED state emits unlocked event", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + LockState:build_test_attr_report(mock_device, LockState.UNLOCKED), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "lock_state: NOT_FULLY_LOCKED state emits unknown event", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + LockState:build_test_attr_report(mock_device, LockState.NOT_FULLY_LOCKED), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.lock.unknown()) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- max_pin_code_length / min_pin_code_length: attribute handlers +-- ============================================================================ + +test.register_coroutine_test( + "max_pin_code_length: emits maxPinCodeLen event", + function() + -- Set SLGA_MIGRATED so we route to the new handlers + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.attributes.MaxPINCodeLength:build_test_attr_report(mock_device, 8), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.maxPinCodeLen(8, { visibility = { displayed = false } }) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "min_pin_code_length: emits minPinCodeLen event", + function() + -- Set SLGA_MIGRATED so we route to the new handlers + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.attributes.MinPINCodeLength:build_test_attr_report(mock_device, 4), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.minPinCodeLen(4, { visibility = { displayed = false } }) + ) + ) + test.wait_for_events() + end +) + +-- ============================================================================ +-- number_of_pin_users_supported: attribute handler +-- ============================================================================ + +test.register_coroutine_test( + "number_of_pin_users_supported: emits pinUsersSupported and totalUsersSupported", + function() + -- Set SLGA_MIGRATED so we route to the new handlers + mock_device:set_field(constants.DRIVER_STATE.SLGA_MIGRATED, true, {}) + + test.socket.zigbee:__queue_receive({ + mock_device.id, + DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 20), + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockCredentials.pinUsersSupported(20, { visibility = { displayed = false } }) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", + capabilities.lockUsers.totalUsersSupported(20, { visibility = { displayed = false } }) + ) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "number_of_pin_users_supported: triggers profile migration when device has no lockCodes", + function() + test.socket.zigbee:__queue_receive({ + mock_device_no_lock_codes.id, + DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device_no_lock_codes, 10), + }) + + -- Should trigger profile update to base-lock + mock_device_no_lock_codes:expect_metadata_update({ profile = "base-lock" }) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "number_of_pin_users_supported: zero value does not trigger profile migration", + function() + test.socket.zigbee:__queue_receive({ + mock_device_no_lock_codes.id, + DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device_no_lock_codes, 0), + }) + + -- No profile update should occur (value is 0) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua index 4b5b18a70e..882d8f5efe 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_samsungsds.lua @@ -30,6 +30,7 @@ local SAMSUNG_SDS_MFR_CODE = 0x0003 local mock_device = test.mock_device.build_test_zigbee_device( { profile = t_utils.get_profile_definition("lock-without-codes.yml"), + provisioning_state = "TYPED", zigbee_endpoints = { [1] = { id = 1, @@ -49,6 +50,20 @@ end test.set_test_init_function(test_init) +local constants = require "lock_utils.constants" +test.register_coroutine_test( + "Device init function handler", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init"}) + test.socket.capability:__set_channel_ordering("relaxed") + test.wait_for_events() + assert(mock_device:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true, "Device init did not set migrated field to true") + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Configure should configure all necessary attributes", function() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua deleted file mode 100644 index 0b083f8050..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-bad-battery-reporter.lua +++ /dev/null @@ -1,41 +0,0 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - --- Mock out globals -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local zigbee_test_utils = require "integration_test.zigbee_test_utils" - -local clusters = require "st.zigbee.zcl.clusters" -local capabilities = require "st.capabilities" -local PowerConfiguration = clusters.PowerConfiguration - -local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("base-lock.yml"), zigbee_endpoints ={ [1] = {id = 1, manufacturer ="Yale", model ="YRD220/240 TSDB", server_clusters = {}} } }) - -zigbee_test_utils.prepare_zigbee_env_info() -local function test_init() - test.mock_device.add_test_device(mock_device)end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Battery percentage report should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, - 55) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.battery.battery(55)) - } - }, - { - min_api_version = 17 - } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua deleted file mode 100644 index 4e9c92bafe..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale-fingerprint-lock.lua +++ /dev/null @@ -1,41 +0,0 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - --- Mock out globals -local test = require "integration_test" -local t_utils = require "integration_test.utils" -local zigbee_test_utils = require "integration_test.zigbee_test_utils" - -local clusters = require "st.zigbee.zcl.clusters" -local capabilities = require "st.capabilities" -local DoorLock = clusters.DoorLock - -local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("base-lock.yml"), zigbee_endpoints ={ [1] = {id = 1, manufacturer ="ASSA ABLOY iRevo", model ="iZBModule01", server_clusters = {}} } }) - -zigbee_test_utils.prepare_zigbee_env_info() -local function test_init() - test.mock_device.add_test_device(mock_device)end - -test.set_test_init_function(test_init) - -test.register_message_test( - "Max user code number report should be handled", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, - 16) } - }, - { - channel = "capability", - direction = "send", - message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(30)) - } - }, - { - min_api_version = 17 - } -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_fingerprint-lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_fingerprint-lock.lua new file mode 100644 index 0000000000..b0f4d1ffcf --- /dev/null +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_fingerprint-lock.lua @@ -0,0 +1,78 @@ +-- Copyright 2022 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Mock out globals +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local DoorLock = clusters.DoorLock +local Alarm = clusters.Alarms + +local mock_device = test.mock_device.build_test_zigbee_device({ profile = t_utils.get_profile_definition("base-lock.yml"), zigbee_endpoints ={ [1] = {id = 1, manufacturer ="ASSA ABLOY iRevo", model ="iZBModule01", server_clusters = {}} } }) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device)end + +local function test_init_new_capabilities() + test.mock_device.add_test_device(mock_device) + test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCodes.ID, command = "migrate", args = {} } }) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.supportedCredentials({"pin"}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockUsers.users({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCredentials.credentials({}, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, Alarm.attributes.AlarmCount:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.MaxPINCodeLength:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.MinPINCodeLength:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device) }) +end + +test.register_message_test( + "Max user code number report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, + 16) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCodes.maxCodes(30)) + } + }, + { + test_init = test_init, + min_api_version = 17 + } +) + +test.register_message_test( + "Max user code number report should be handled for migrated locks", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, + 16) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockCredentials.pinUsersSupported(30)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.lockUsers.totalUsersSupported(30)) + } + }, + { test_init = test_init_new_capabilities } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua similarity index 100% rename from drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale.lua rename to drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua index a80632bf80..48a6521059 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/can_handle.lua @@ -2,6 +2,9 @@ -- Licensed under the Apache License, Version 2.0 local yale_fingerprint_lock_models = function(opts, driver, device) + local consts = require "lock_utils.constants" + local slga_migrated = device:get_field(consts.DRIVER_STATE.SLGA_MIGRATED) or false + if not slga_migrated then return false end local FINGERPRINTS = require("yale-fingerprint-lock.fingerprints") for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then diff --git a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua index b78d043784..9965ef181e 100644 --- a/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua +++ b/drivers/SmartThings/zigbee-lock/src/yale-fingerprint-lock/init.lua @@ -5,14 +5,16 @@ local clusters = require "st.zigbee.zcl.clusters" local capabilities = require "st.capabilities" local LockCluster = clusters.DoorLock -local LockCodes = capabilities.lockCodes +local LockCredentials = capabilities.lockCredentials +local LockUsers = capabilities.lockUsers local YALE_FINGERPRINT_MAX_CODES = 0x1E local handle_max_codes = function(driver, device, value) - device:emit_event(LockCodes.maxCodes(YALE_FINGERPRINT_MAX_CODES), { visibility = { displayed = false } }) + device:emit_event(LockCredentials.pinUsersSupported(YALE_FINGERPRINT_MAX_CODES)) + device:emit_event(LockUsers.totalUsersSupported(YALE_FINGERPRINT_MAX_CODES)) end local yale_fingerprint_lock_driver = { @@ -24,7 +26,7 @@ local yale_fingerprint_lock_driver = { } } }, - can_handle = require("yale-fingerprint-lock.can_handle"), + can_handle = require("yale-fingerprint-lock.can_handle") } return yale_fingerprint_lock_driver diff --git a/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua b/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua deleted file mode 100644 index 4b546979d3..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/yale/sub_drivers.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local lazy_load_if_possible = require "lazy_load_subdriver" -local sub_drivers = { - lazy_load_if_possible("yale.yale-bad-battery-reporter"), -} -return sub_drivers diff --git a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua b/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua deleted file mode 100644 index 67169e9268..0000000000 --- a/drivers/SmartThings/zigbee-lock/src/yale/yale-bad-battery-reporter/can_handle.lua +++ /dev/null @@ -1,14 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local is_bad_yale_lock_models = function(opts, driver, device) - local FINGERPRINTS = require("yale.yale-bad-battery-reporter.fingerprints") - for _, fingerprint in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - return true, require("yale.yale-bad-battery-reporter") - end - end - return false -end - -return is_bad_yale_lock_models From 4dcfe01730600802f2de171304f7213529a484ad Mon Sep 17 00:00:00 2001 From: HunsupJung <59987061+HunsupJung@users.noreply.github.com> Date: Fri, 10 Jul 2026 02:30:04 +0900 Subject: [PATCH 023/102] Matter Lock: Switch sub-driver and main-driver to support SLGA migration (#2842) --------- Signed-off-by: Hunsup Jung Co-authored-by: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Co-authored-by: Harrison Carter --- drivers/SmartThings/matter-lock/src/init.lua | 3234 ++++++++++++++--- .../matter-lock/src/lock_utils.lua | 34 + .../src/new-matter-lock/can_handle.lua | 19 - .../src/new-matter-lock/fingerprints.lua | 50 - .../matter-lock/src/new-matter-lock/init.lua | 3049 ---------------- .../matter-lock/src/sub_drivers.lua | 2 +- .../src/test/test_bridged_matter_lock.lua | 4 +- .../matter-lock/src/test/test_matter_lock.lua | 9 +- .../src/test/test_matter_lock_battery.lua | 4 - .../test/test_matter_lock_batteryLevel.lua | 1 + .../src/test/test_matter_lock_codes.lua | 68 +- .../src/test/test_matter_lock_cota.lua | 2 - .../src/test/test_matter_lock_migration.lua | 143 + .../src/test/test_matter_lock_modular.lua | 2 +- .../src/test/test_new_matter_lock.lua | 223 +- .../src/test/test_new_matter_lock_aliro.lua | 693 +++- .../src/test/test_new_matter_lock_battery.lua | 190 + .../src/unmigrated-matter-lock/can_handle.lua | 17 + .../src/unmigrated-matter-lock/init.lua | 758 ++++ 19 files changed, 4750 insertions(+), 3752 deletions(-) delete mode 100644 drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua delete mode 100644 drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua delete mode 100644 drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua create mode 100644 drivers/SmartThings/matter-lock/src/test/test_matter_lock_migration.lua create mode 100644 drivers/SmartThings/matter-lock/src/unmigrated-matter-lock/can_handle.lua create mode 100644 drivers/SmartThings/matter-lock/src/unmigrated-matter-lock/init.lua diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 09f384ee4e..5d82801b67 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -1,33 +1,490 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - local MatterDriver = require "st.matter.driver" +local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" +local im = require "st.matter.interaction_model" +local utils = require "st.utils" +local lock_utils = require "lock_utils" + +local version = require "version" +if version.api < 10 then + clusters.DoorLock = require "DoorLock" +end local DoorLock = clusters.DoorLock local PowerSource = clusters.PowerSource -local capabilities = require "st.capabilities" -local im = require "st.matter.interaction_model" -local lock_utils = require "lock_utils" +local INITIAL_CREDENTIAL_INDEX = 1 +local ALL_INDEX = 0xFFFE +-- maximum as defined by the Matter specification +local MAX_USER_NAME_LENGTH = 10 + +local RESPONSE_STATUS_MAP = { + [DoorLock.types.DlStatus.SUCCESS] = "success", + [DoorLock.types.DlStatus.FAILURE] = "failure", + [DoorLock.types.DlStatus.DUPLICATE] = "duplicate", + [DoorLock.types.DlStatus.OCCUPIED] = "occupied", + [DoorLock.types.DlStatus.INVALID_FIELD] = "invalidCommand", + [DoorLock.types.DlStatus.RESOURCE_EXHAUSTED] = "resourceExhausted", + [DoorLock.types.DlStatus.NOT_FOUND] = "failure" +} + +local WEEK_DAY_MAP = { + ["Sunday"] = 1, + ["Monday"] = 2, + ["Tuesday"] = 4, + ["Wednesday"] = 8, + ["Thursday"] = 16, + ["Friday"] = 32, + ["Saturday"] = 64, +} -local PROFILE_BASE_NAME = "__profile_base_name" +local ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP = { + ["evictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_EVICTABLE_ENDPOINT_KEY, + ["nonEvictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY +} -local INITIAL_COTA_INDEX = 1 +local battery_support = { + NO_BATTERY = "NO_BATTERY", + BATTERY_LEVEL = "BATTERY_LEVEL", + BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" +} --- add this definition for locks to work on older lua libs -local UNLATCHED_STATE = 0x3 +local profiling_data = { + BATTERY_SUPPORT = "__BATTERY_SUPPORT", +} +local DoorLockFeatureMapAttr = {ID = 0xFFFC, cluster = DoorLock.ID} local subscribed_attributes = { - [capabilities.lock.ID] = {DoorLock.attributes.LockState}, - [capabilities.battery.ID] = {PowerSource.attributes.BatPercentRemaining}, - [capabilities.batteryLevel.ID] = {PowerSource.attributes.BatChargeLevel}, + [capabilities.lock.ID] = { + DoorLock.attributes.LockState + }, + [capabilities.remoteControlStatus.ID] = { + DoorLock.attributes.OperatingMode + }, + [capabilities.lockUsers.ID] = { + DoorLock.attributes.NumberOfTotalUsersSupported + }, + [capabilities.lockCredentials.ID] = { + DoorLock.attributes.NumberOfPINUsersSupported, + DoorLock.attributes.MaxPINCodeLength, + DoorLock.attributes.MinPINCodeLength, + DoorLock.attributes.RequirePINforRemoteOperation + }, + [capabilities.lockSchedules.ID] = { + DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser, + DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser + }, + [capabilities.lockAliro.ID] = { + DoorLock.attributes.AliroReaderVerificationKey, + DoorLock.attributes.AliroReaderGroupIdentifier, + DoorLock.attributes.AliroReaderGroupSubIdentifier, + DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions, + DoorLock.attributes.AliroGroupResolvingKey, + DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions, + DoorLock.attributes.AliroBLEAdvertisingVersion, + DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported, + DoorLock.attributes.NumberOfAliroEndpointKeysSupported, + }, + [capabilities.battery.ID] = { + PowerSource.attributes.BatPercentRemaining + }, + [capabilities.batteryLevel.ID] = { + PowerSource.attributes.BatChargeLevel + } } ---- If a device needs a cota credential this function attempts to set the credential ---- at the index provided. The set_credential_response_handler handles all failures ---- and retries with the appropriate index when necessary. +local subscribed_events = { + [capabilities.lock.ID] = { + DoorLock.events.LockOperation + }, + [capabilities.lockAlarm.ID] = { + DoorLock.events.DoorLockAlarm + }, + [capabilities.lockUsers.ID] = { + DoorLock.events.LockUserChange + } +} + +local function find_default_endpoint(device, cluster) + local res = device.MATTER_DEFAULT_ENDPOINT + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then --0 is the matter RootNode endpoint + return v + end + end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) + return res +end + +local function component_to_endpoint(device, component_name) + return find_default_endpoint(device, clusters.DoorLock.ID) +end + +local function device_init(driver, device) + device:set_component_to_endpoint_fn(component_to_endpoint) + if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, {persist = true}) + elseif device:get_field(profiling_data.BATTERY_SUPPORT) == nil then + device:add_subscribed_attribute(clusters.PowerSource.attributes.AttributeList) + end + for cap_id, attributes in pairs(subscribed_attributes) do + if device:supports_capability_by_id(cap_id) then + for _, attr in ipairs(attributes) do + device:add_subscribed_attribute(attr) + end + end + end + device:add_subscribed_attribute(DoorLockFeatureMapAttr) + for cap_id, events in pairs(subscribed_events) do + if device:supports_capability_by_id(cap_id) then + for _, e in ipairs(events) do + device:add_subscribed_event(e) + end + end + end + device:subscribe() + end + +local function device_added(driver, device) + device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) +end + +local function set_reader_config(device) + local reader_config_updated = device:get_field(lock_utils.ALIRO_READER_CONFIG_UPDATED) or nil + if reader_config_updated == "TRUE" or reader_config_updated == "IN_PROGRESS" then return end + + local cmdName = "setReaderConfig" + local groupId = lock_utils.create_group_id_resolving_key() + local groupResolvingKey = nil + local aliro_ble_uwb_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.ALIROBLEUWB}) + if #aliro_ble_uwb_eps > 0 then + groupResolvingKey = lock_utils.create_group_id_resolving_key() + end + local privKey, pubKey = lock_utils.generate_keypair(device) + if not privKey or not pubKey then + local command_result_info = { + commandName = cmdName, + statusCode = "failure" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.VERIFICATION_KEY, pubKey, {persist = true}) + device:set_field(lock_utils.GROUP_ID, groupId, {persist = true}) + device:set_field(lock_utils.GROUP_RESOLVING_KEY, groupResolvingKey, {persist = true}) + + -- Send command + local ep = find_default_endpoint(device, clusters.DoorLock.ID) + device:send( + DoorLock.server.commands.SetAliroReaderConfig( + device, ep, + lock_utils.hex_string_to_octet_string(privKey), + lock_utils.hex_string_to_octet_string(pubKey), + lock_utils.hex_string_to_octet_string(groupId), + lock_utils.hex_string_to_octet_string(groupResolvingKey) + ) + ) + device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "IN_PROGRESS", {persist = true}) +end + +local function match_profile_modular(driver, device) + local enabled_optional_component_capability_pairs = {} + local main_component_capabilities = {} + local modular_profile_name = "lock-modular" + for _, device_ep in pairs(device.endpoints) do + for _, ep_cluster in pairs(device_ep.clusters) do + if ep_cluster.cluster_id == DoorLock.ID then + local clus_has_feature = function(feature_bitmap) + return DoorLock.are_features_supported( + feature_bitmap, + lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, device_ep.endpoint_id) or + ep_cluster.feature_map + ) + end + if clus_has_feature(DoorLock.types.Feature.USER) then + table.insert(main_component_capabilities, capabilities.lockUsers.ID) + end + if clus_has_feature(DoorLock.types.Feature.PIN_CREDENTIAL) then + table.insert(main_component_capabilities, capabilities.lockCredentials.ID) + end + if clus_has_feature(DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES) or + clus_has_feature(DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES) then + table.insert(main_component_capabilities, capabilities.lockSchedules.ID) + end + if clus_has_feature(DoorLock.types.Feature.UNBOLT) then + device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + modular_profile_name = "lock-modular-embedded-unlatch" -- use the embedded config specified in this profile for devices supporting "unlatch" + else + device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + end + if clus_has_feature(DoorLock.types.Feature.ALIRO_PROVISIONING) then + table.insert(main_component_capabilities, capabilities.lockAliro.ID) + end + break + end + end + end + + local supported_battery_type = device:get_field(profiling_data.BATTERY_SUPPORT) + if supported_battery_type == battery_support.BATTERY_LEVEL then + table.insert(main_component_capabilities, capabilities.batteryLevel.ID) + elseif supported_battery_type == battery_support.BATTERY_PERCENTAGE then + table.insert(main_component_capabilities, capabilities.battery.ID) + end + + table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) + if modular_profile_name == "lock-modular-embedded-unlatch" -- the embedded config that may be needed is not checked by an optional capability comparison + or device:supports_capability(capabilities.lockCodes) -- always re-profile a device using the legacy code storage methods + or lock_utils.optional_capabilities_list_changed(enabled_optional_component_capability_pairs, device.profile.components) then + device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) + end +end + +local function match_profile_switch(driver, device) + local user_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.USER}) + local pin_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.PIN_CREDENTIAL}) + local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) + local year_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES}) + local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) + + local profile_name = "lock" + if #user_eps > 0 then + profile_name = profile_name .. "-user" + if #pin_eps > 0 then + profile_name = profile_name .. "-pin" + end + if #week_schedule_eps + #year_schedule_eps > 0 then + profile_name = profile_name .. "-schedule" + end + end + if #unbolt_eps > 0 then + profile_name = profile_name .. "-unlatch" + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + else + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + end + + local supported_battery_type = device:get_field(profiling_data.BATTERY_SUPPORT) + if supported_battery_type == battery_support.BATTERY_LEVEL then + profile_name = profile_name .. "-batteryLevel" + elseif supported_battery_type == battery_support.BATTERY_PERCENTAGE then + profile_name = profile_name .. "-battery" + end + + device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) +end + +--- Deeply compare two values. +--- Handles metatables. Can optionally ignore cycle checking and/or function differences. +--- +--- @param a any +--- @param b any +--- @param opts table|nil { ignore_functions = boolean, ignore_cycles = boolean } +--- @param seen table|nil +--- @return boolean +local function deep_equals(a, b, opts, seen) + if a == b then return true end -- same object + if type(a) ~= type(b) then return false end -- different type + if type(a) == "function" and opts and opts.ignore_functions then return true end + if type(a) ~= "table" then return false end -- same type but not table, thus was already compared + + -- check for cycles in table references and preserve reference topology. + if not (opts and opts.ignore_cycles) then + seen = seen or {} + seen[a] = seen[a] or {} + if seen[a][b] then + return seen[a][b] + end + seen[a][b] = true + end + + -- Compare keys/values from a + for k, v in pairs(a) do + if not deep_equals(v, b[k], opts, seen) then + return false + end + end + + -- Ensure b doesn't have extra keys + for k in pairs(b) do + if a[k] == nil then + return false + end + end + + -- Compare metatables + local mt_a = getmetatable(a) + local mt_b = getmetatable(b) + return deep_equals(mt_a, mt_b, opts, seen) +end + +local function migrate_lock_codes(device) + local lockCodes = device:get_field(lock_utils.LOCK_CODES_FOR_MIGRATION) or {} + local user_table = {} + local credential_table = {} + for _, entry in ipairs(lockCodes) do + local index = tonumber(entry[1]) + table.insert(user_table, {userIndex = index, userType = "guest", userName = entry[2]}) + table.insert(credential_table, {userIndex = index, credentialIndex = index, credentialType = "pin"}) + end + if #user_table > 0 or #credential_table > 0 then + device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) + device:emit_event(capabilities.lockCredentials.credentials(credential_table, {visibility = {displayed = false}})) + end +end + +local function info_changed(driver, device, event, args) + if deep_equals(device.profile, args.old_st_store.profile, { ignore_functions = true }) then + return + end + for cap_id, attributes in pairs(subscribed_attributes) do + if device:supports_capability_by_id(cap_id) then + for _, attr in ipairs(attributes) do + device:add_subscribed_attribute(attr) + end + end + end + for cap_id, events in pairs(subscribed_events) do + if device:supports_capability_by_id(cap_id) then + for _, e in ipairs(events) do + device:add_subscribed_event(e) + end + end + end + device:subscribe() + if device:supports_capability_by_id(capabilities.lockAliro.ID) then + set_reader_config(device) + end + if device:get_latest_state("main", capabilities.lockAlarm.ID, capabilities.lockAlarm.supportedAlarmValues.NAME) == nil then + device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) + device:emit_event(capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) -- lockJammed is mandatory + end + if device:get_field(lock_utils.LOCK_CODES_COPY_REQUIRED) == true then + migrate_lock_codes(device) + device:set_field(lock_utils.LOCK_CODES_COPY_REQUIRED, false, {persist = true}) + end +end + +local function profiling_data_still_required(device) + for _, field in pairs(profiling_data) do + if device:get_field(field) == nil then + return true -- data still required if a field is nil + end + end + return false +end + +local function match_profile(driver, device, ignore_static_profiling) + if profiling_data_still_required(device) then return end + if version.api >= 15 and version.rpc >= 9 then + match_profile_modular(driver, device) + elseif ignore_static_profiling ~= true then + match_profile_switch(driver, device) + end +end + +local function do_configure(driver, device) + match_profile(driver, device, false) + device.thread:call_with_delay(5, function() + device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) + device:emit_event(capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) -- lockJammed is mandatory + end) +end + +local function driver_switched(driver, device) + match_profile(driver, device, false) + device:try_update_metadata({provisioning_state = "PROVISIONED"}) +end + +-- Matter Handler +local function lock_state_handler(driver, device, ib, response) + local LockState = DoorLock.attributes.LockState + local attr = capabilities.lock.lock + local LOCK_STATE = { + [LockState.NOT_FULLY_LOCKED] = attr.not_fully_locked(), + [LockState.LOCKED] = attr.locked(), + [LockState.UNLOCKED] = attr.unlocked(), + [LockState.UNLATCHED] = attr.unlatched() + } + + -- The lock state is usually updated in lock_state_handler and lock_op_event_handler, respectively. + -- In this case, two events occur. To prevent this, when both functions are called, + -- it send the event after 1 second so that no event occurs in the lock_state_handler. + device.thread:call_with_delay(1, function () + if ib.data.value ~= nil then + device:emit_event(LOCK_STATE[ib.data.value]) + else + device.log.warn("Lock State is nil") + end + end) +end + +local function operating_modes_handler(driver, device, ib, response) + local status = capabilities.remoteControlStatus.remoteControlEnabled + local op_type = DoorLock.types.OperatingModeEnum + local opMode_map = { + [op_type.NORMAL] = true, + [op_type.VACATION] = true, + [op_type.PRIVACY] = false, + [op_type.NO_REMOTE_LOCK_UNLOCK] = false, + [op_type.PASSAGE] = false, + } + local result = opMode_map[ib.data.value] + local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) + if result == true then + device:emit_event(status("true", {visibility = {displayed = true}})) + if #unbolt_eps > 0 then + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) + else + device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + end + elseif result == false then + device:emit_event(status("false", {visibility = {displayed = true}})) + device:emit_event(capabilities.lock.supportedLockCommands({}, {visibility = {displayed = false}})) + end +end + +local function total_users_supported_handler(driver, device, ib, response) + device:emit_event(capabilities.lockUsers.totalUsersSupported(ib.data.value, {visibility = {displayed = false}})) +end + +local function pin_users_supported_handler(driver, device, ib, response) + device:emit_event(capabilities.lockCredentials.pinUsersSupported(ib.data.value, {visibility = {displayed = false}})) +end + +local function min_pin_code_len_handler(driver, device, ib, response) + device:emit_event(capabilities.lockCredentials.minPinCodeLen(ib.data.value, {visibility = {displayed = false}})) +end + +local function max_pin_code_len_handler(driver, device, ib, response) + device:emit_event(capabilities.lockCredentials.maxPinCodeLen(ib.data.value, {visibility = {displayed = false}})) +end + local function set_cota_credential(device, credential_index) local eps = device:get_endpoints(DoorLock.ID) local cota_cred = device:get_field(lock_utils.COTA_CRED) @@ -40,7 +497,8 @@ local function set_cota_credential(device, credential_index) return end - if device:get_field(lock_utils.SET_CREDENTIAL) ~= nil then + -- Check Busy State + if lock_utils.is_busy_state_set(device) then device.log.debug("delaying setting COTA credential since a credential is currently being set") device.thread:call_with_delay(2, function(t) set_cota_credential(device, credential_index) @@ -48,50 +506,161 @@ local function set_cota_credential(device, credential_index) return end + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, "addCota") + device:set_field(lock_utils.CRED_INDEX, credential_index) device:set_field(lock_utils.COTA_CRED_INDEX, credential_index, {persist = true}) - local credential = {credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = credential_index} - -- Set the credential to a code - device:set_field(lock_utils.SET_CREDENTIAL, credential_index) + device:set_field(lock_utils.USER_TYPE, "remote") + + -- Send command device.log.info(string.format("Attempting to set COTA credential at index %s", credential_index)) + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.PIN, + credential_index = credential_index + } device:send(DoorLock.server.commands.SetCredential( device, #eps > 0 and eps[1] or 1, - DoorLock.types.DlDataOperationType.ADD, + DoorLock.types.DataOperationTypeEnum.ADD, credential, device:get_field(lock_utils.COTA_CRED), nil, -- nil user_index creates a new user - DoorLock.types.DlUserStatus.OCCUPIED_ENABLED, - DoorLock.types.DlUserType.REMOTE_ONLY_USER + DoorLock.types.UserStatusEnum.OCCUPIED_ENABLED, + DoorLock.types.UserTypeEnum.REMOTE_ONLY_USER )) end local function generate_cota_cred_for_device(device) - local len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) or 4 + local len = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) or 6 local cred_data = math.floor(math.random() * (10 ^ len)) cred_data = string.format("%0" .. tostring(len) .. "d", cred_data) device:set_field(lock_utils.COTA_CRED, cred_data, {persist = true}) end -local function lock_state_handler(driver, device, ib, response) - local LockState = DoorLock.attributes.LockState - local attr = capabilities.lock.lock - local LOCK_STATE = { - [LockState.NOT_FULLY_LOCKED] = attr.not_fully_locked(), - [LockState.LOCKED] = attr.locked(), - [LockState.UNLOCKED] = attr.unlocked(), - [UNLATCHED_STATE] = attr.unlocked(), -- Fully unlocked with latch pulled - } +local function apply_cota_credentials_if_absent(device) + if not device:get_field(lock_utils.COTA_CRED) then + -- Process after all other info blocks have been dispatched to ensure MaxPINCodeLength has been processed + device.thread:call_with_delay(0, function(t) + generate_cota_cred_for_device(device) + -- delay needed to allow test to override the random credential data + device.thread:call_with_delay(0, function(t) + -- Attempt to set cota credential at the lowest index + set_cota_credential(device, INITIAL_CREDENTIAL_INDEX) + end) + end) + end +end + +local function require_remote_pin_handler(driver, device, ib, response) + if ib.data.value then + apply_cota_credentials_if_absent(device) + else + device:set_field(lock_utils.COTA_CRED, false, {persist = true}) + end +end + +local function max_week_schedule_of_user_handler(driver, device, ib, response) + device:emit_event(capabilities.lockSchedules.weekDaySchedulesPerUser(ib.data.value, {visibility = {displayed = false}})) +end +local function max_year_schedule_of_user_handler(driver, device, ib, response) + device:emit_event(capabilities.lockSchedules.yearDaySchedulesPerUser(ib.data.value, {visibility = {displayed = false}})) +end + +local function aliro_reader_verification_key_handler(driver, device, ib, response) if ib.data.value ~= nil then - local event = LOCK_STATE[ib.data.value] - if event ~= nil then - device:emit_event(event) - else - device.log.warn(string.format("Received unknown lock state value (%s), emitting unknown", ib.data.value)) - device:emit_event(attr.unknown()) + device:emit_event(capabilities.lockAliro.readerVerificationKey( + utils.bytes_to_hex_string(ib.data.value), {visibility = {displayed = false}} + )) + device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "TRUE", {persist = true}) + end +end + +local function aliro_reader_group_id_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.readerGroupIdentifier( + utils.bytes_to_hex_string(ib.data.value), + {visibility = {displayed = false}} + )) + end +end + +local function aliro_group_resolving_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.groupResolvingKey( + utils.bytes_to_hex_string(ib.data.value), + {visibility = {displayed = false}} + )) + end +end + +local function aliro_protocol_versions_handler(driver, device, ib, response) + if ib.data.elements == nil then + return + end + local protocol_versions = {} + for i, element in ipairs(ib.data.elements) do + local ver = string.format("%s.%s", element.value:byte(1), element.value:byte(2)) + table.insert(protocol_versions, ver); + end + device:emit_event(capabilities.lockAliro.expeditedTransactionProtocolVersions(protocol_versions, {visibility = {displayed = false}})) +end + +local function aliro_supported_ble_uwb_protocol_versions_handler(driver, device, ib, response) + if ib.data.elements == nil then + return + end + local protocol_versions = {} + for i, element in ipairs(ib.data.elements) do + local ver = string.format("%s.%s", element.value:byte(1), element.value:byte(2)) + table.insert(protocol_versions, ver); + end + device:emit_event(capabilities.lockAliro.bleUWBProtocolVersions(protocol_versions, {visibility = {displayed = false}})) +end + +local function aliro_ble_advertising_version_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.bleAdvertisingVersion(string.format("%s", ib.data.value), {visibility = {displayed = false}})) + end +end + +local function max_aliro_credential_issuer_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.maxCredentialIssuerKeys(ib.data.value, {visibility = {displayed = false}})) + end +end + +local function max_aliro_endpoint_key_handler(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.lockAliro.maxEndpointKeys(ib.data.value, {visibility = {displayed = false}})) + end +end + +local function door_lock_feature_map_handler(driver, device, ib, response) + if ib.data.value == nil then return end + local feature_map = lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id) or nil + if feature_map ~= ib.data.value then + lock_utils.set_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id, ib.data.value, { persist = true }) + match_profile(driver, device, true) + end +end + +local function handle_power_source_attribute_list(driver, device, ib, response) + local latest_battery_support = device:get_field(profiling_data.BATTERY_SUPPORT) + for _, attr in ipairs(ib.data.elements or {}) do + if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE, {persist=true}) + break -- BATTERY_PERCENTAGE is highest priority. break early if found + elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID then + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_LEVEL, {persist=true}) end - else - device:emit_event(LOCK_STATE[LockState.NOT_FULLY_LOCKED]) + end + -- in the case that 1) no battery has been set, and 2) the returned ib does not include battery attributes, ignore battery + if latest_battery_support == nil and not device:get_field(profiling_data.BATTERY_SUPPORT) then + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, {persist=true}) + end + if latest_battery_support == nil or latest_battery_support ~= device:get_field(profiling_data.BATTERY_SUPPORT) then + match_profile(driver, device, false) end end @@ -99,632 +668,2235 @@ local function handle_battery_percent_remaining(driver, device, ib, response) if ib.data.value ~= nil then device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) end -end +end + +local function handle_battery_charge_level(driver, device, ib, response) + if ib.data.value == PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end +end + +-- Capability Handler +local function handle_lock(driver, device, command) + local ep = device:component_to_endpoint(command.component) + local cota_cred = device:get_field(lock_utils.COTA_CRED) + if cota_cred then + device:send( + DoorLock.server.commands.LockDoor(device, ep, cota_cred) + ) + else + device:send(DoorLock.server.commands.LockDoor(device, ep)) + end +end + +local function handle_unlock(driver, device, command) + local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) + local cota_cred = device:get_field(lock_utils.COTA_CRED) + local ep = device:component_to_endpoint(command.component) + + if #unbolt_eps > 0 then + if cota_cred then + device:send( + DoorLock.server.commands.UnboltDoor(device, ep, cota_cred) + ) + else + device:send(DoorLock.server.commands.UnboltDoor(device, ep)) + end + else + if cota_cred then + device:send( + DoorLock.server.commands.UnlockDoor(device, ep, cota_cred) + ) + else + device:send(DoorLock.server.commands.UnlockDoor(device, ep)) + end + end +end + +local function handle_unlatch(driver, device, command) + local ep = device:component_to_endpoint(command.component) + local cota_cred = device:get_field(lock_utils.COTA_CRED) + if cota_cred then + device:send( + DoorLock.server.commands.UnlockDoor(device, ep, cota_cred) + ) + else + device:send(DoorLock.server.commands.UnlockDoor(device, ep)) + end +end + +local function add_user_to_table(device, userIdx, userName, userType) + -- Get latest user table + local user_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockUsers.ID, + capabilities.lockUsers.users.NAME, + {} + )) + + -- Add new entry to table + table.insert(user_table, {userIndex = userIdx, userName = userName, userType = userType}) + device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) +end + +local function update_user_in_table(device, userIdx, userName, userType) + -- Get latest user table + local user_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockUsers.ID, + capabilities.lockUsers.users.NAME, + {} + )) + + -- Find user entry + local i = 0 + for index, entry in pairs(user_table) do + if entry.userIndex == userIdx then + i = index + break + end + end + + -- Update user entry + if i ~= 0 then + user_table[i].userType = userType + user_table[i].userName = userName + device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) + end +end + +local function delete_user_from_table(device, userIdx) + -- If User Index is ALL_INDEX, remove all entry from the table + if userIdx == ALL_INDEX then + device:emit_event(capabilities.lockUsers.users({}, {visibility = {displayed = false}})) + return + end + + -- Get latest user table + local user_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockUsers.ID, + capabilities.lockUsers.users.NAME, + {} + )) + + -- Remove element from user table + for index, entry in pairs(user_table) do + if entry.userIndex == userIdx then + table.remove(user_table, index) + break + end + end + device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) +end + +local function has_credentials(device, userIdx) + -- Get latest credential table + local cred_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockCredentials.ID, + capabilities.lockCredentials.credentials.NAME, + {} + )) + + -- Find credential + for index, entry in pairs(cred_table) do + if entry.userIndex == userIdx then + return true + end + end + + -- Get latest Aliro credential table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find Aliro credential + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx then + return true + end + end + + return false +end + +local function add_credential_to_table(device, userIdx, credIdx, credType) + -- Get latest credential table + local cred_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockCredentials.ID, + capabilities.lockCredentials.credentials.NAME, + {} + )) + + -- Add new entry to table + table.insert(cred_table, {userIndex = userIdx, credentialIndex = credIdx, credentialType = credType}) + device:emit_event(capabilities.lockCredentials.credentials(cred_table, {visibility = {displayed = false}})) +end + +local function delete_credential_from_table(device, credIdx) + -- If Credential Index is ALL_INDEX, remove all entries from the table + if credIdx == ALL_INDEX then + device:emit_event(capabilities.lockCredentials.credentials({}, {visibility = {displayed = false}})) + return ALL_INDEX + end + + -- Get latest credential table + local cred_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockCredentials.ID, + capabilities.lockCredentials.credentials.NAME, + {} + )) + + -- Delete an entry from credential table + local userIdx = nil + for index, entry in pairs(cred_table) do + if entry.credentialIndex == credIdx then + table.remove(cred_table, index) + userIdx = entry.userIndex + break + end + end + + device:emit_event(capabilities.lockCredentials.credentials(cred_table, {visibility = {displayed = false}})) + return userIdx +end + +local function delete_credential_from_table_as_user(device, userIdx) + -- If User Index is ALL_INDEX, remove all entry from the table + if userIdx == ALL_INDEX then + device:emit_event(capabilities.lockCredentials.credentials({}, {visibility = {displayed = false}})) + return + end + + -- Get latest credential table + local cred_table = device:get_latest_state( + "main", + capabilities.lockCredentials.ID, + capabilities.lockCredentials.credentials.NAME + ) or {} + local new_cred_table = {} + + -- Re-create credential table + for index, entry in pairs(cred_table) do + if entry.userIndex ~= userIdx then + table.insert(new_cred_table, entry) + end + end + + device:emit_event(capabilities.lockCredentials.credentials(new_cred_table, {visibility = {displayed = false}})) +end + +local function add_week_schedule_to_table(device, userIdx, scheduleIdx, schedule) + -- Get latest week day schedule table + local week_schedule_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockSchedules.ID, + capabilities.lockSchedules.weekDaySchedules.NAME, + {} + )) + + -- Find schedule for specific user + local i = 0 + for index, entry in pairs(week_schedule_table) do + if entry.userIndex == userIdx then + i = index + end + end + + -- Re-create weekDays list + local weekDayList = {} + for _, weekday in ipairs(schedule.weekDays) do + table.insert(weekDayList, weekday) + end + + if i ~= 0 then -- Add schedule for existing user + -- Exclude same scheduleIdx + local new_schedule_table = {} + for index, entry in pairs(week_schedule_table[i].schedules) do + if entry.scheduleIndex ~= scheduleIdx then + table.insert(new_schedule_table, entry) + end + end + -- Add new entry to table + table.insert( + new_schedule_table, + { + scheduleIndex = scheduleIdx, + weekDays = weekDayList, + startHour = schedule.startHour, + startMinute = schedule.startMinute, + endHour = schedule.endHour, + endMinute = schedule.endMinute + } + ) + -- Update schedule for specific user + week_schedule_table[i].schedules = new_schedule_table + else -- Add schedule for new user + table.insert( + week_schedule_table, + { + userIndex = userIdx, + schedules = {{ + scheduleIndex = scheduleIdx, + weekDays = weekDayList, + startHour = schedule.startHour, + startMinute = schedule.startMinute, + endHour = schedule.endHour, + endMinute = schedule.endMinute + }} + } + ) + end + + device:emit_event(capabilities.lockSchedules.weekDaySchedules(week_schedule_table, {visibility = {displayed = false}})) +end + +local function delete_week_schedule_from_table(device, userIdx, scheduleIdx) + -- Get latest week day schedule table + local week_schedule_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockSchedules.ID, + capabilities.lockSchedules.weekDaySchedules.NAME, + {} + )) + + -- Find schedule for specific user + local i = 0 + for index, entry in pairs(week_schedule_table) do + if entry.userIndex == userIdx then + i = index + end + end + + -- When there is no userIndex in the table + if i == 0 then + return + end + + -- Re-create schedule table for the user + local new_schedule_table = {} + for index, entry in pairs(week_schedule_table[i].schedules) do + if entry.scheduleIndex ~= scheduleIdx then + table.insert(new_schedule_table, entry) + end + end + + -- If user has no schedule, remove user from the table + if #new_schedule_table == 0 then + table.remove(week_schedule_table, i) + else + week_schedule_table[i].schedules = new_schedule_table + end + + device:emit_event(capabilities.lockSchedules.weekDaySchedules(week_schedule_table, {visibility = {displayed = false}})) +end + +local function delete_week_schedule_from_table_as_user(device, userIdx) + -- If User Index is ALL_INDEX, remove all entry from the table + if userIdx == ALL_INDEX then + device:emit_event(capabilities.lockSchedules.weekDaySchedules({}, {visibility = {displayed = false}})) + return + end + + -- Get latest week day schedule table + local week_schedule_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockSchedules.ID, + capabilities.lockSchedules.weekDaySchedules.NAME, + {} + )) + + -- Re-create week day schedule table + local new_week_schedule_table = {} + for index, entry in pairs(week_schedule_table) do + if entry.userIndex ~= userIdx then + table.insert(new_week_schedule_table, entry) + end + end + + device:emit_event(capabilities.lockSchedules.weekDaySchedules(new_week_schedule_table, {visibility = {displayed = false}})) +end + +local function add_year_schedule_to_table(device, userIdx, scheduleIdx, sTime, eTime) + -- Get latest year day schedule table + local year_schedule_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockSchedules.ID, + capabilities.lockSchedules.yearDaySchedules.NAME, + {} + )) + + -- Find schedule for specific user + local i = 0 + for index, entry in pairs(year_schedule_table) do + if entry.userIndex == userIdx then + i = index + end + end + + if i ~= 0 then -- Add schedule for existing user + -- Exclude same scheduleIdx + local new_schedule_table = {} + for index, entry in pairs(year_schedule_table[i].schedules) do + if entry.scheduleIndex ~= scheduleIdx then + table.insert(new_schedule_table, entry) + end + end + -- Add new entry to table + table.insert( + new_schedule_table, + { + scheduleIndex = scheduleIdx, + localStartTime = sTime, + localEndTime = eTime + } + ) + -- Update schedule for specific user + year_schedule_table[i].schedules = new_schedule_table + else -- Add schedule for new user + table.insert( + year_schedule_table, + { + userIndex = userIdx, + schedules = {{ + scheduleIndex = scheduleIdx, + localStartTime = sTime, + localEndTime = eTime + }} + } + ) + end + + device:emit_event(capabilities.lockSchedules.yearDaySchedules(year_schedule_table, {visibility = {displayed = false}})) +end + +local function delete_year_schedule_from_table(device, userIdx, scheduleIdx) + -- Get latest year day schedule table + local year_schedule_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockSchedules.ID, + capabilities.lockSchedules.yearDaySchedules.NAME, + {} + )) + + -- Find schedule for specific user + local i = 0 + for index, entry in pairs(year_schedule_table) do + if entry.userIndex == userIdx then + i = index + end + end + + -- When there is no userIndex in the table + if i == 0 then + return + end + + -- Re-create year day schedule table for the user + local new_schedule_table = {} + for index, entry in pairs(year_schedule_table[i].schedules) do + if entry.scheduleIndex ~= scheduleIdx then + table.insert(new_schedule_table, entry) + end + end + + -- If user has no schedule, remove user from the table + if #new_schedule_table == 0 then + table.remove(year_schedule_table, i) + else + year_schedule_table[i].schedules = new_schedule_table + end + + device:emit_event(capabilities.lockSchedules.yearDaySchedules(year_schedule_table, {visibility = {displayed = false}})) +end + +local function delete_year_schedule_from_table_as_user(device, userIdx) + -- If User Index is ALL_INDEX, remove all entry from the table + if userIdx == ALL_INDEX then + device:emit_event(capabilities.lockSchedules.yearDaySchedules({}, {visibility = {displayed = false}})) + return + end + + -- Get latest year day schedule table + local year_schedule_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockSchedules.ID, + capabilities.lockSchedules.yearDaySchedules.NAME, + {} + )) + + -- Re-create year day schedule table + local new_year_schedule_table = {} + for index, entry in pairs(year_schedule_table) do + if entry.userIndex ~= userIdx then + table.insert(new_year_schedule_table, entry) + end + end + + device:emit_event(capabilities.lockSchedules.yearDaySchedules(new_year_schedule_table, {visibility = {displayed = false}})) +end + +local function add_aliro_to_table(device, userIdx, keyIdx, keyType, keyId) + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Add new entry to table + table.insert(aliro_table, {userIndex = userIdx, keyIndex = keyIdx, keyType = keyType, keyId = keyId}) + device:emit_event(capabilities.lockAliro.credentials(aliro_table, {visibility = {displayed = false}})) +end + +local function delete_aliro_from_table(device, userIdx, keyType, keyId) + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Delete an entry from aliro table + if keyType == "issuerKey" then + for i, entry in pairs(aliro_table) do + if entry.userIndex == userIdx then + table.remove(aliro_table, i) + break + end + end + else + for i, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyId == keyId then + table.remove(aliro_table, i) + break + end + end + end + device:emit_event(capabilities.lockAliro.credentials(aliro_table, {visibility = {displayed = false}})) +end + +local function delete_aliro_from_table_as_user(device, userIdx) + -- If User Index is ALL_INDEX, remove all entry from the table + if userIdx == ALL_INDEX then + device:emit_event(capabilities.lockAliro.credentials({}, {visibility = {displayed = false}})) + return + end + + -- Get latest credential table + local aliro_table = device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME + ) or {} + local new_aliro_table = {} + + -- Re-create credential table + for index, entry in pairs(aliro_table) do + if entry.userIndex ~= userIdx then + table.insert(new_aliro_table, entry) + end + end + + device:emit_event(capabilities.lockAliro.credentials(new_aliro_table, {visibility = {displayed = false}})) +end + +local function handle_add_user(driver, device, command) + -- Get parameters + local cmdName = "addUser" + local userName = command.args.userName + local userType = command.args.userType + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) + device:set_field(lock_utils.USER_NAME, userName, {persist = true}) + device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) + + -- Get available user index + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) +end + +local function handle_update_user(driver, device, command) + -- Get parameters + local cmdName = "updateUser" + local userIdx = command.args.userIndex + local userName = command.args.userName + local userNameMatter = string.sub(userName, 1, MAX_USER_NAME_LENGTH) + local userType = command.args.userType + local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + if userType == "guest" then + userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER + end + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.USER_NAME, userName, {persist = true}) + device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetUser( + device, ep, + DoorLock.types.DataOperationTypeEnum.MODIFY, + userIdx, + userNameMatter, + nil, -- Unique ID + nil, -- User Status + userTypeMatter, + nil -- Credential Rule + ) + ) +end + +local function get_user_response_handler(driver, device, ib, response) + local elements = ib.info_block.data.elements + local userIdx = elements.user_index.value + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + end + if status ~= "success" then + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + statusCode = status + } + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + return + end + + local ep = find_default_endpoint(device, DoorLock.ID) + local status = elements.user_status.value + local maxUser = device:get_latest_state( + "main", + capabilities.lockUsers.ID, + capabilities.lockUsers.totalUsersSupported.NAME + ) or 10 + -- Found available user index + if status == nil or status == DoorLock.types.UserStatusEnum.AVAILABLE then + local userName = device:get_field(lock_utils.USER_NAME) + local userNameMatter = string.sub(userName, 1, MAX_USER_NAME_LENGTH) + local userType = device:get_field(lock_utils.USER_TYPE) + local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + if userType == "guest" then + userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER + end + + -- Save values to field + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + + -- Send command + device:send( + DoorLock.server.commands.SetUser( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, + userIdx, + userNameMatter, + nil, -- Unique ID + nil, -- User Status + userTypeMatter, + nil -- Credential Rule + ) + ) + elseif userIdx >= maxUser then -- There's no available user index + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + statusCode = "resourceExhausted" + } + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + else -- Check next user index + device:send(DoorLock.server.commands.GetUser(device, ep, userIdx + 1)) + end +end + +local function set_user_response_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userName = device:get_field(lock_utils.USER_NAME) + local userType = device:get_field(lock_utils.USER_TYPE) + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.OCCUPIED then + status = "occupied" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + end + + -- Update User in table + if status == "success" then + if cmdName == "addUser" then + add_user_to_table(device, userIdx, userName, userType) + elseif cmdName == "updateUser" then + update_user_in_table(device, userIdx, userName, userType) + end + else + device.log.warn(string.format("Failed to set user: %s", status)) + end + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + statusCode = status + } + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) +end + +local function handle_delete_user(driver, device, command) + -- Get parameters + local cmdName = "deleteUser" + local userIdx = command.args.userIndex + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearUser(device, ep, userIdx)) +end + +local function handle_delete_all_users(driver, device, command) + -- Get parameters + local cmdName = "deleteAllUsers" + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, ALL_INDEX, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearUser(device, ep, ALL_INDEX)) +end + +local function clear_user_response_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local userIdx = device:get_field(lock_utils.USER_INDEX) + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + end + + -- Delete User and Credential from table + if status == "success" then + delete_user_from_table(device, userIdx) + delete_credential_from_table_as_user(device, userIdx) + delete_aliro_from_table_as_user(device, userIdx) + delete_week_schedule_from_table_as_user(device, userIdx) + delete_year_schedule_from_table_as_user(device, userIdx) + else + device.log.warn(string.format("Failed to clear user: %s", status)) + end + + -- In the "defaultSchedule" cmd failure path, when a guest user's credentials are set but the scheduling + -- fails during default setup, those credentials should be removed, so we wait to log lock credentials until here. + if cmdName == "defaultSchedule" then + -- note: if clear user succeeds, we'd log credential settings as a "failure" since it's effectively a no-op. + -- If clear user fails, log "success" since the credentials would still be present. + local lock_credential_status = status == "success" and "failure" or "success" + local command_result_info = { + commandName = "addCredential", + userIndex = userIdx, + statusCode = lock_credential_status + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + else + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + statusCode = status + } + device:emit_event(capabilities.lockUsers.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + end + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) +end + +local function handle_add_credential(driver, device, command) + -- Get parameters + local cmdName = "addCredential" + local userIdx = command.args.userIndex + local userType = command.args.userType + local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + if userType == "guest" then + userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER + end + if userIdx == 0 then + userIdx = nil + else + userTypeMatter = nil + end + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.PIN, + credential_index = INITIAL_CREDENTIAL_INDEX + } + local credData = command.args.credentialData + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) + device:set_field(lock_utils.CRED_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) + device:set_field(lock_utils.CRED_DATA, credData, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + credData, -- Credential Data + userIdx, -- User Index + nil, -- User Status + userTypeMatter -- User Type + ) + ) +end + +local function handle_update_credential(driver, device, command) + -- Get parameters + local cmdName = "updateCredential" + local userIdx = command.args.userIndex + local credIdx = command.args.credentialIndex + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.PIN, + credential_index = credIdx + } + local credData = command.args.credentialData + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) + device:set_field(lock_utils.USER_TYPE, nil, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.MODIFY, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + credData, -- Credential Data + userIdx, -- User Index + nil, -- User Status + nil -- User Type + ) + ) +end + +local function set_pin_response_handler(driver, device, ib, response) + if ib.status ~= im.InteractionResponse.Status.SUCCESS then + device.log.error("Failed to set credential for device") + return + end + + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local credData = device:get_field(lock_utils.CRED_DATA) + if cmdName == "addCota" then + credData = device:get_field(lock_utils.COTA_CRED) + end + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userType = device:get_field(lock_utils.USER_TYPE) + local credIdx = device:get_field(lock_utils.CRED_INDEX) + local elements = ib.info_block.data.elements + local status = RESPONSE_STATUS_MAP[elements.status.value] + + if status == "success" then + -- Don't save user and credential for COTA + if cmdName == "addCota" then + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + return + end + + -- If user is added also, update User table + if userIdx == nil then + add_user_to_table(device, elements.user_index.value, nil, userType) + end + + -- Update Credential table + userIdx = elements.user_index.value + if cmdName == "addCredential" then + add_credential_to_table(device, userIdx, credIdx, "pin") + end + + -- If User Type is Guest and device support schedule, add default schedule + local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) + local year_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES}) + if userType == "guest" and (#week_schedule_eps > 0 or #year_schedule_eps > 0) and cmdName ~= "updateCredential" then + local cmdName = "defaultSchedule" + local scheduleIdx = 1 + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) + + local ep = device:component_to_endpoint("main") + device:send( + DoorLock.server.commands.SetYearDaySchedule( + device, ep, + scheduleIdx, + userIdx, + lock_utils.MIN_EPOCH_S, + lock_utils.MAX_EPOCH_S + ) + ) + else + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + credentialIndex = credIdx, + statusCode = status + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end + return + end + + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. + device.log.warn(string.format("Failed to set credential: %s", status)) + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { + commandName = cmdName, + statusCode = "resourceExhausted" -- No more available credential index + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + elseif status == "occupied" then + -- Get parameters + local credIdx = elements.next_credential_index.value + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.PIN, + credential_index = credIdx, + } + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userType = device:get_field(lock_utils.USER_TYPE) + local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + if userIdx ~= nil then + userTypeMatter = nil + elseif userType == "guest" then + userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER + elseif userType == "remote" then + userTypeMatter = DoorLock.types.UserTypeEnum.REMOTE_ONLY_USER + end + + device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = find_default_endpoint(device, DoorLock.ID) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + credData, -- Credential Data + userIdx, -- User Index + nil, -- User Status + userTypeMatter -- User Type + ) + ) + elseif status == "duplicate" and cmdName == "addCota" then + generate_cota_cred_for_device(device) + device.thread:call_with_delay(0, function(t) set_cota_credential(device, credIdx) end) + else + local command_result_info = { + commandName = cmdName, + statusCode = status + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end +end + +local function set_issuer_key_response_handler(driver, device, ib, response) + local cmdName = "setIssuerKey" + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local issuerKeyIndex = device:get_field(lock_utils.ISSUER_KEY_INDEX) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + local elements = ib.info_block.data.elements + local status = RESPONSE_STATUS_MAP[elements.status.value] + + if status == "success" then + -- Delete field data + device:set_field(lock_utils.ISSUER_KEY, nil, {persist = true}) + + -- If user is added also, update User table + if userIdx == nil then + userIdx = elements.user_index.value + add_user_to_table(device, userIdx, nil, "adminMember") + end + + -- Update Aliro table + add_aliro_to_table(device, userIdx, issuerKeyIndex, "issuerKey", nil) + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + return + end + + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. + device.log.warn(string.format("Failed to set credential: %s", status)) + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "resourceExhausted" -- No more available credential index + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + elseif status == "occupied" then + -- Get parameters + if userIdx ~= nil then + userType = nil + end + local credIdx = elements.next_credential_index.value + local credType = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY + local credData = device:get_field(lock_utils.ISSUER_KEY) + local credential = { + credential_type = credType, + credential_index = credIdx + } + + -- Save values to field + device:set_field(lock_utils.ISSUER_KEY_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = find_default_endpoint(device, DoorLock.ID) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, + credential, -- Credential + lock_utils.hex_string_to_octet_string(credData), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) + else + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end +end + +local function set_endpoint_key_response_handler(driver, device, ib, response) + local cmdName = "setEndpointKey" + local userIdx = device:get_field(lock_utils.USER_INDEX) + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local keyId = device:get_field(lock_utils.DEVICE_KEY_ID) + local keyType = device:get_field(lock_utils.ENDPOINT_KEY_TYPE) + local endpointKeyIndex = device:get_field(lock_utils.ENDPOINT_KEY_INDEX) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + local elements = ib.info_block.data.elements + local status = RESPONSE_STATUS_MAP[elements.status.value] + + if status == "success" then + -- Delete field data + device:set_field(lock_utils.ENDPOINT_KEY, nil, {persist = true}) + + -- If user is added also, update User table + if userIdx == nil then + userIdx = elements.user_index.value + add_user_to_table(device, userIdx, nil, "adminMember") + end + + -- Update Aliro table + add_aliro_to_table(device, userIdx, endpointKeyIndex, keyType, keyId) + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + return + end + + -- In the case DlStatus returns Occupied, this means the current credential index is in use, + -- so we must try the next one. If there is not a next index (i.e. it is nil), + -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. + device.log.warn(string.format("Failed to set credential: %s", status)) + + if status == "occupied" and elements.next_credential_index.value == nil then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "resourceExhausted" -- No more available credential index + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + elseif status == "occupied" then + -- Get parameters + if userIdx ~= nil then + userType = nil + end + local credIdx = elements.next_credential_index.value + local credType = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType] + local credData = device:get_field(lock_utils.ENDPOINT_KEY) + local credential = { + credential_type = credType, + credential_index = credIdx + } + + -- Save values to field + device:set_field(lock_utils.ENDPOINT_KEY_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = find_default_endpoint(device, DoorLock.ID) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, + credential, -- Credential + lock_utils.hex_string_to_octet_string(credData), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) + else + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + end +end + +local function set_credential_response_handler(driver, device, ib, response) + if ib.status ~= im.InteractionResponse.Status.SUCCESS then + device.log.error("Failed to set credential for device") + return + end + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + if cmdName == "addCredential" or cmdName == "updateCredential" or cmdName == "addCota" then + set_pin_response_handler(driver, device, ib, response) + elseif cmdName == "setIssuerKey" then + set_issuer_key_response_handler(driver, device, ib, response) + elseif cmdName == "setEndpointKey" then + set_endpoint_key_response_handler(driver, device, ib, response) + end +end + +local function handle_delete_credential(driver, device, command) + -- Get parameters + local cmdName = "deleteCredential" + local credIdx = command.args.credentialIndex + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.PIN, + credential_index = credIdx, + } + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) +end + +local function handle_delete_all_credentials(driver, device, command) + -- Get parameters + local cmdName = "deleteAllCredentials" + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.PIN, + credential_index = ALL_INDEX, + } + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.CRED_INDEX, ALL_INDEX, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) +end + +local function clear_credential_response_handler(driver, device, ib, response) + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + if cmdName ~= "deleteCredential" and cmdName ~= "clearEndpointKey" and + cmdName ~= "clearIssuerKey" and cmdName ~= "deleteAllCredentials" then + return + end + local status = RESPONSE_STATUS_MAP[ib.status] or "success" + local command_result_info = { commandName = cmdName, statusCode = status } -- default command result + local userIdx = device:get_field(lock_utils.USER_INDEX) + local all_user_credentials_removed = false + + if (cmdName == "deleteCredential" or cmdName == "deleteAllCredentials") and status == "success" then + -- Get result from data saved in relevant, associated fields + local credIdx = device:get_field(lock_utils.CRED_INDEX) + + -- find userIdx associated with credIdx, don't use lock utils field in this case + userIdx = delete_credential_from_table(device, credIdx) + if userIdx ~= nil then + all_user_credentials_removed = not has_credentials(device, userIdx) + end + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.credentialIndex = credIdx + elseif cmdName == "clearIssuerKey" and status == "success" then + -- Get result from data saved in relevant, associated fields + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + + delete_aliro_from_table(device, userIdx, "issuerKey", nil) + all_user_credentials_removed = not has_credentials(device, userIdx) + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.requestId = reqId + elseif cmdName == "clearEndpointKey" and status == "success" then + -- Get result from data saved in relevant, associated fields + local deviceKeyId = device:get_field(lock_utils.DEVICE_KEY_ID) + local keyType = device:get_field(lock_utils.ENDPOINT_KEY_TYPE) + local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) + + delete_aliro_from_table(device, userIdx, keyType, deviceKeyId) + all_user_credentials_removed = not has_credentials(device, userIdx) + + -- set unique command result fields + command_result_info.userIndex = userIdx + command_result_info.keyId = deviceKeyId + command_result_info.requestId = reqId + end + + -- user data if credentials were removed + if all_user_credentials_removed then + delete_user_from_table(device, userIdx) + delete_week_schedule_from_table_as_user(device, userIdx) + delete_year_schedule_from_table_as_user(device, userIdx) + end -local function handle_battery_charge_level(driver, device, ib, response) - if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then - device:emit_event(capabilities.batteryLevel.battery.normal()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then - device:emit_event(capabilities.batteryLevel.battery.warning()) - elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then - device:emit_event(capabilities.batteryLevel.battery.critical()) + -- Update commandResult + if cmdName == "deleteCredential" or cmdName == "deleteAllCredentials" then + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + else + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) end + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end -local function handle_power_source_attribute_list(driver, device, ib, response) - local support_battery_percentage = false - local support_battery_level = false - for _, attr in ipairs(ib.data.elements) do - -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or - -- BatChargeLevel (Attribute ID 0x0E) is present. - if attr.value == 0x0C then - support_battery_percentage = true - elseif attr.value == 0x0E then - support_battery_level = true - end - end - local profile_name = device:get_field(PROFILE_BASE_NAME) - if profile_name ~= nil then - if not support_battery_percentage then - if support_battery_level then - profile_name = profile_name .. "-batteryLevel" - else - profile_name = profile_name .. "-nobattery" - end - end - device.log.info(string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) +local function handle_set_week_day_schedule(driver, device, command) + -- Get parameters + local cmdName = "setWeekDaySchedule" + local scheduleIdx = command.args.scheduleIndex + local userIdx = command.args.userIndex + local schedule = command.args.schedule + local wDays = {} + local scheduleBit = 0 + for _, weekDay in ipairs(schedule.weekDays) do + scheduleBit = scheduleBit + WEEK_DAY_MAP[weekDay] + table.insert(wDays, weekDay) end -end + local startHour = schedule.startHour + local startMinute = schedule.startMinute + local endHour = schedule.endHour + local endMinute = schedule.endMinute -local function max_pin_code_len_handler(driver, device, ib, response) - device:emit_event(capabilities.lockCodes.maxCodeLength(ib.data.value, {visibility = {displayed = false}})) -end + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end -local function min_pin_code_len_handler(driver, device, ib, response) - device:emit_event(capabilities.lockCodes.minCodeLength(ib.data.value, {visibility = {displayed = false}})) -end + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) + device:set_field(lock_utils.SCHEDULE_WEEK_DAYS, wDays, {persist = true}) + device:set_field(lock_utils.SCHEDULE_START_HOUR, startHour, {persist = true}) + device:set_field(lock_utils.SCHEDULE_START_MINUTE, startMinute, {persist = true}) + device:set_field(lock_utils.SCHEDULE_END_HOUR, endHour, {persist = true}) + device:set_field(lock_utils.SCHEDULE_END_MINUTE, endMinute, {persist = true}) -local function num_pin_users_handler(driver, device, ib, response) - device:set_field(lock_utils.TOTAL_PIN_USERS, ib.data.value) - device:emit_event(capabilities.lockCodes.maxCodes(ib.data.value, {visibility = {displayed = false}})) + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetWeekDaySchedule( + device, ep, + scheduleIdx, -- Week Day Schedule Index + userIdx, -- User Index + scheduleBit, -- Days Mask + startHour, -- Start Hour + startMinute, -- Start Minute + endHour, -- End Hour + endMinute -- End Minute + ) + ) end -local function apply_cota_credentials_if_absent(device) - if not device:get_field(lock_utils.COTA_CRED) then - --Process after all other info blocks have been dispatched to ensure MaxPINCodeLength has been processed - device.thread:call_with_delay(0, function(t) - generate_cota_cred_for_device(device) - -- delay needed to allow test to override the random credential data - device.thread:call_with_delay(0, function(t) - -- Attempt to set cota credential at the lowest index - set_cota_credential(device, INITIAL_COTA_INDEX) - end) - end) +local function set_week_day_schedule_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local userIdx = device:get_field(lock_utils.USER_INDEX) + local scheduleIdx = device:get_field(lock_utils.SCHEDULE_INDEX) + local days = device:get_field(lock_utils.SCHEDULE_WEEK_DAYS) + local sHour = device:get_field(lock_utils.SCHEDULE_START_HOUR) + local sMinute = device:get_field(lock_utils.SCHEDULE_START_MINUTE) + local eHour = device:get_field(lock_utils.SCHEDULE_END_HOUR) + local eMinute = device:get_field(lock_utils.SCHEDULE_END_MINUTE) + local schedule = { + weekDays = days, + startHour = sHour, + startMinute = sMinute, + endHour = eHour, + endMinute = eMinute + } + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" end -end -local function require_remote_pin_handler(driver, device, ib, response) - if ib.data.value then - apply_cota_credentials_if_absent(device) + -- Add Week Day Schedule to table + if status == "success" then + add_week_schedule_to_table(device, userIdx, scheduleIdx, schedule) else - device:set_field(lock_utils.COTA_CRED, false, {persist = true}) + device.log.warn(string.format("Failed to set week day schedule: %s", status)) end + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + scheduleIndex = scheduleIdx, + statusCode = status + } + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end -local function clear_credential_response_handler(driver, device, ib, response) - local deleted_code_slot = device:get_field(lock_utils.DELETING_CODE) - if deleted_code_slot == nil and ib.status == im.InteractionResponse.Status.SUCCESS then - device.log.debug("Cleared space in lock credential db for COTA credential") +local function handle_clear_week_day_schedule(driver, device, command) + -- Get parameters + local cmdName = "clearWeekDaySchedules" + local scheduleIdx = command.args.scheduleIndex + local userIdx = command.args.userIndex + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end - if ib.status == im.InteractionResponse.Status.SUCCESS then - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, tostring(deleted_code_slot))) - --make sure cota credential exists if the user deletes it or if space was created for the COTA cred - if deleted_code_slot == device:get_field(lock_utils.COTA_CRED_INDEX) or - device:get_field(lock_utils.NONFUNCTIONAL) then - set_cota_credential(device, device:get_field(lock_utils.COTA_CRED_INDEX) or INITIAL_COTA_INDEX) - end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearWeekDaySchedule(device, ep, scheduleIdx, userIdx)) +end + +local function clear_week_day_schedule_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local scheduleIdx = device:get_field(lock_utils.SCHEDULE_INDEX) + local userIdx = device:get_field(lock_utils.USER_INDEX) + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + end + + -- Delete Week Day Schedule to table + if status == "success" then + delete_week_schedule_from_table(device, userIdx, scheduleIdx) else - device.log.error(string.format("Failed to delete code slot %s", deleted_code_slot)) + device.log.warn(string.format("Failed to clear week day schedule: %s", status)) end - device:set_field(lock_utils.DELETING_CODE, nil) + + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + scheduleIndex = scheduleIdx, + statusCode = status + } + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end -local function set_credential_response_handler(driver, device, ib, response) - if ib.status ~= im.InteractionResponse.Status.SUCCESS then - device.log.error("Failed to set code for device") +local function handle_set_year_day_schedule(driver, device, command) + -- Get parameters + local cmdName = "setYearDaySchedule" + local scheduleIdx = command.args.scheduleIndex + local userIdx = command.args.userIndex + local localStartTime = command.args.schedule.localStartTime + local localEndTime = command.args.schedule.localEndTime + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end - local elements = ib.info_block.data.elements - local credential_index = device:get_field(lock_utils.SET_CREDENTIAL) - device:set_field(lock_utils.SET_CREDENTIAL, nil) - if credential_index == nil then - device.log.error("Received unexpected SetCredentialResponse") - return - end - local code_slot = tostring(credential_index) - local status = elements.status.value - if status == DoorLock.types.DlStatus.SUCCESS then - local event = capabilities.lockCodes.codeChanged("", {state_change = true}) - local cota_cred_index = device:get_field(lock_utils.COTA_CRED_INDEX) - local code_name = (credential_index == cota_cred_index and lock_utils.COTA_CODE_NAME) or - lock_utils.get_code_name(device, code_slot) - event.data = {codeName = code_name} - event.value = lock_utils.get_change_type(device, tostring(code_slot)) - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = event.data.codeName - device:emit_event(event) - if credential_index == cota_cred_index then - device:emit_event( - capabilities.lockCodes.codeChanged( - code_slot .. " renamed", {state_change = true} - ) - ) - end - lock_utils.lock_codes_event(device, lock_codes) - lock_utils.reset_code_state(device, code_slot) - if device:get_field(lock_utils.NONFUNCTIONAL) and cota_cred_index == credential_index then - device.log.info("Successfully set COTA credential after being non-functional") - device:set_field(lock_utils.NONFUNCTIONAL, false, {persist = true}) - local profile_name = "base-lock" - device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) - local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) - if #battery_feature_eps == 0 then - profile_name = profile_name .. "-nobattery" - device.log.info(string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name, provisioning_state = "PROVISIONED"}) - else - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - req:merge(clusters.PowerSource.attributes.AttributeList:read()) - device:send(req) - end - end - elseif device:get_field(lock_utils.COTA_CRED) and credential_index == device:get_field(lock_utils.COTA_CRED_INDEX) then - -- Handle failure to set a COTA credential - if status == DoorLock.types.DlStatus.OCCUPIED and elements.next_credential_index.value ~= nil then - --This credential index is unavailable, but there is another available - set_cota_credential(device, elements.next_credential_index.value) - elseif status == DoorLock.types.DlStatus.OCCUPIED and - elements.next_credential_index.value == nil and - credential_index == INITIAL_COTA_INDEX then - --There are no credential indices available on the device - device.log.error("Device requires COTA credential, but has no credential indexes available!") - device.log.error("Lock and Unlock commands will no longer work!!") - device:set_field(PROFILE_BASE_NAME, "nonfunctional-lock", {persist = true}) - device:try_update_metadata({profile = "nonfunctional-lock", provisioning_state = "NONFUNCTIONAL"}) - device:set_field(lock_utils.NONFUNCTIONAL, true, {persist = true}) - elseif status == DoorLock.types.DlStatus.OCCUPIED and elements.next_credential_index.value == nil then - --There are no credential indices available, but we must ensure we search all indices. - set_cota_credential(device, INITIAL_COTA_INDEX) - elseif status == DoorLock.types.DlStatus.DUPLICATE then - --The credential we randomly generated already exists - generate_cota_cred_for_device(device) - --delay 0 needed for unit test verification of random value - device.thread:call_with_delay(0, function(t) set_cota_credential(device, credential_index) end) - elseif status == DoorLock.types.DlStatus.INVALID_FIELD then - device.log.error("Invalid SetCredential command sent to set a COTA credential. This is a bug.") - elseif elements.next_credential_index.value ~= nil then - device.log.warn(string.format( - "Received non-success SetCredentialResponse status (%s), but there is a next credential index available", elements.status - )) - set_cota_credential(device, elements.next_credential_index.value) - end - else - device.log.error( - string.format( - "Failed to set user code for device, SetCredential status received: %s", elements.status - ) + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) + device:set_field(lock_utils.SCHEDULE_LOCAL_START_TIME, localStartTime, {persist = true}) + device:set_field(lock_utils.SCHEDULE_LOCAL_END_TIME, localEndTime, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetYearDaySchedule( + device, ep, + scheduleIdx, + userIdx, + lock_utils.iso8601_to_epoch(localStartTime), + lock_utils.iso8601_to_epoch(localEndTime) ) - end + ) end -local function get_credential_status_response_handler(driver, device, ib, response) - if ib.status ~= im.InteractionResponse.Status.SUCCESS then - device.log.warn("Not taking action on GetCredentialStatusResponse because failed status") +local function set_year_day_schedule_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local userIdx = device:get_field(lock_utils.USER_INDEX) + local scheduleIdx = device:get_field(lock_utils.SCHEDULE_INDEX) + local localStartTime = device:get_field(lock_utils.SCHEDULE_LOCAL_START_TIME) + local localEndTime = device:get_field(lock_utils.SCHEDULE_LOCAL_END_TIME) + local status = "success" + if ib.status == DoorLock.types.DlStatus.FAILURE then + status = "failure" + elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" end - local cred_index = device:get_field(lock_utils.CHECKING_CREDENTIAL) - if cred_index == nil then - device.log.warn("Received unexpected CredentialStatusResponse") + + if cmdName == "defaultSchedule" then + local cmdName = "addCredential" + local credIdx = device:get_field(lock_utils.CRED_INDEX) + + if status == "success" then + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + credentialIndex = credIdx, + statusCode = status + } + device:emit_event(capabilities.lockCredentials.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + else + local ep = find_default_endpoint(device, clusters.DoorLock.ID) + device:send(DoorLock.server.commands.ClearUser(device, ep, userIdx)) + end return end - local elements = ib.info_block.data.elements - local credential_exists = elements.credential_exists.value - local next_credential_index = elements.next_credential_index and elements.next_credential_index.value or nil - - local event = capabilities.lockCodes.codeChanged("", {state_change = true}) - local code_slot = tostring(cred_index) - local cota_cred_index = device:get_field(lock_utils.COTA_CRED_INDEX) - local code_name = (cred_index == cota_cred_index and lock_utils.COTA_CODE_NAME) or lock_utils.get_code_name(device, code_slot) - event.data = {codeName = code_name} - if credential_exists then - -- Code slot is occupied - event.value = lock_utils.get_change_type(device, code_slot) - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = event.data.codeName - device:emit_event(event) - lock_utils.lock_codes_event(device, lock_codes) - lock_utils.reset_code_state(device, code_slot) - else - -- Code slot is unoccupied - if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then - -- Code has been deleted - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) - if cred_index == cota_cred_index then --make sure cota credential exists if it was deleted - set_cota_credential(device, INITIAL_COTA_INDEX) - end - else - -- Code is unset - event.value = code_slot .. " unset" - device:emit_event(event) + + if status == "success" then + if cmdName == "setYearDaySchedule" then + add_year_schedule_to_table(device, userIdx, scheduleIdx, localStartTime, localEndTime) + elseif cmdName == "clearYearDaySchedules" then + delete_year_schedule_from_table(device, userIdx, scheduleIdx) end + else + device.log.warn(string.format("Failed to set/clear year day schedule: %s", status)) end - device:set_field(lock_utils.CHECKING_CREDENTIAL, nil) - local is_scanning = device:get_latest_state( - "main", capabilities.lockCodes.ID, capabilities.lockCodes.scanCodes.NAME - ) == "Scanning" - if not is_scanning then + -- Update commandResult + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + scheduleIndex = scheduleIdx, + statusCode = status + } + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) +end + +local function handle_clear_year_day_schedule(driver, device, command) + -- Get parameters + local cmdName = "clearYearDaySchedules" + local scheduleIdx = command.args.scheduleIndex + local userIdx = command.args.userIndex + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockSchedules.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) return end - if (next_credential_index == nil) then - device:emit_event( - capabilities.lockCodes.scanCodes( - "Complete", {visibility = {displayed = false}} - ) - ) - local lock_codes = lock_utils.get_lock_codes(device) - lock_utils.lock_codes_event(device, lock_codes) - elseif next_credential_index ~= nil then - device:set_field(lock_utils.CHECKING_CREDENTIAL, next_credential_index) - device:send( - DoorLock.server.commands.GetCredentialStatus( - device, - ib.info_block.endpoint_id, - {credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = device:get_field(lock_utils.CHECKING_CREDENTIAL)} - ) + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + + -- Send command + -- In SmartThings, Schedule Restrict User is basically allowed to access always. + -- So, if user delete the year day schedule, enter an infinitely long schedule. + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetYearDaySchedule( + device, ep, + scheduleIdx, + userIdx, + lock_utils.MIN_EPOCH_S, + lock_utils.MAX_EPOCH_S ) - end + ) end local function alarm_event_handler(driver, device, ib, response) local DlAlarmCode = DoorLock.types.DlAlarmCode local alarm_code = ib.data.elements.alarm_code - if alarm_code.value == DlAlarmCode.FRONT_ESCEUTCHEON_REMOVED or alarm_code.value - == DlAlarmCode.WRONG_CODE_ENTRY_LIMIT or alarm_code.value == DlAlarmCode.FORCED_USER - or alarm_code.value == DlAlarmCode.DOOR_FORCED_OPEN then - device:emit_event(capabilities.tamperAlert.tamper.detected()) + if alarm_code.value == DlAlarmCode.LOCK_JAMMED then + device:emit_event(capabilities.lockAlarm.alarm.unableToLockTheDoor({state_change = true})) + elseif alarm_code.value == DlAlarmCode.LOCK_FACTORY_RESET then + device:emit_event(capabilities.lockAlarm.alarm.lockFactoryReset({state_change = true})) + elseif alarm_code.value == DlAlarmCode.WRONG_CODE_ENTRY_LIMIT then + device:emit_event(capabilities.lockAlarm.alarm.attemptsExceeded({state_change = true})) + elseif alarm_code.value == DlAlarmCode.FRONT_ESCEUTCHEON_REMOVED then + device:emit_event(capabilities.lockAlarm.alarm.damaged({state_change = true})) + elseif alarm_code.value == DlAlarmCode.DOOR_FORCED_OPEN then + device:emit_event(capabilities.lockAlarm.alarm.forcedOpeningAttempt({state_change = true})) end end local function lock_op_event_handler(driver, device, ib, response) - local tamper_detected = device:get_latest_state( - device:endpoint_to_component(ib.endopint_id), - capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME - ) - if nil == tamper_detected or tamper_detected == capabilities.tamperAlert.tamper.detected.NAME then - device:emit_event(capabilities.tamperAlert.tamper.clear()) - end -end - -local function lock_user_change_event_handler(driver, device, ib, response) - local event = capabilities.lockCodes.codeChanged("", {state_change = true}) - local elements = ib.data.elements - local data_type_changed = elements.lock_data_type.value - local operation_type = elements.data_operation_type.value - local user_index = elements.user_index.value - local data_index = elements.data_index and elements.data_index.value - local cota_cred_index = device:get_field(lock_utils.COTA_CRED_INDEX) - - if data_type_changed == DoorLock.types.DlLockDataType.PIN then -- pin added or removed - local code_slot = data_index and tostring(data_index) or nil - if (operation_type == DoorLock.types.DlDataOperationType.ADD or operation_type - == DoorLock.types.DlDataOperationType.MODIFY) and code_slot ~= nil then - local change_type = lock_utils.get_change_type(device, code_slot) - event.value = change_type - local code_name = (data_index == cota_cred_index and lock_utils.COTA_CODE_NAME) or lock_utils.get_code_name(device, code_slot) - event.data = {codeName = code_name} - device:emit_event(event) - if string.match(change_type, "%d+ set") ~= nil then - local lock_codes = lock_utils.get_lock_codes(device) - lock_codes[code_slot] = code_name - lock_utils.lock_codes_event(device, lock_codes) - end - elseif operation_type == DoorLock.types.DlDataOperationType.CLEAR and code_slot ~= nil then - lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, tostring(code_slot))) - --make sure cota credential is created if the user deletes it or a space is made for it - if data_index == cota_cred_index or device:get_field(lock_utils.NONFUNCTIONAL) then - set_cota_credential(device, cota_cred_index or INITIAL_COTA_INDEX) - end - else -- invalid event because no credential index - device.log.error( - "Received unhandled LockUserChangeEvent because it didn't affect a PIN credential" - ) - end - elseif data_type_changed == DoorLock.types.DlLockDataType.USER_INDEX and operation_type - == DoorLock.types.DlDataOperationType.CLEAR then - if user_index == 0xFFFE then - device.log.warn("All users were cleared by another fabric") -- we never do this - for cs, _ in pairs(lock_utils.get_lock_codes(device)) do - lock_utils.code_deleted(device, cs) - end - lock_utils.lock_codes_event(device, {}) - if device:get_field(lock_utils.COTA_CRED) ~= nil then set_cota_credential(device, INITIAL_COTA_INDEX) end - else - device.log.info("Not handling LockUserChange event") - end - -- Note when a Lock User is deleted, the credentials associated with that user are also deleted. - -- Change events are created for each credential as well as the user. + local opType = ib.data.elements.lock_operation_type + local opSource = ib.data.elements.operation_source + local userIdx = ib.data.elements.user_index + -- TODO: This handler can check fabric index and exclude other fabric events + + if opType == nil or opSource == nil then + return + end + + local Type = DoorLock.types.LockOperationTypeEnum + local Lock = capabilities.lock.lock + if opType.value == Type.LOCK then + opType = Lock.locked + elseif opType.value == Type.UNLOCK then + opType = Lock.unlocked + elseif opType.value == Type.UNLATCH then + opType = Lock.unlatched else - device.log.info( - string.format( - "Not handling LockUserChange event because the data type (%s) doesn't affect lock codes", - elements.lock_data_type - ) - ) + return end -end -local function handle_refresh(driver, device, command) - -- Note: no endpoint specified indicates a wildcard endpoint - local req = DoorLock.attributes.LockState:read(device) - req:merge(PowerSource.attributes.BatPercentRemaining:read(device)) - device:send(req) + local Source = DoorLock.types.OperationSourceEnum + if opSource.value == Source.UNSPECIFIED then + opSource = nil + elseif opSource.value == Source.MANUAL then + opSource = "manual" + elseif opSource.value == Source.PROPRIETARY_REMOTE then + opSource = "proprietaryRemote" + elseif opSource.value == Source.KEYPAD then + opSource = "keypad" + elseif opSource.value == Source.AUTO then + opSource = "auto" + elseif opSource.value == Source.BUTTON then + opSource = "button" + elseif opSource.value == Source.SCHEDULE then + opSource = nil + elseif opSource.value == Source.REMOTE then + opSource = "command" + elseif opSource.value == Source.RFID then + opSource = "rfid" + elseif opSource.value == Source.BIOMETRIC then + opSource = nil -- It will be updated R2 + elseif opSource.value == Source.ALIRO then + opSource = "digitalKey" + else + opSource = nil + end + + if userIdx ~= nil then + userIdx = userIdx.value + end + + local data_obj = {method = opSource, userIndex = userIdx} + device:emit_event(opType({data = data_obj, state_change = true})) end -local function handle_lock(driver, device, command) - local ep = device:component_to_endpoint(command.component) - local cota_cred = device:get_field(lock_utils.COTA_CRED) - if cota_cred then - device:send( - DoorLock.server.commands.LockDoor(device, ep, cota_cred) - ) +local function handle_refresh(driver, device, command) + if device:get_field(lock_utils.LOCK_CODES_COPY_REQUIRED) == true then + -- ensure battery state remains as it was before the profile migration + if device:supports_capability(capabilities.battery) then + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE, {persist = true}) + elseif device:supports_capability(capabilities.batteryLevel) then + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_LEVEL, {persist = true}) + else + device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, {persist = true}) + end + match_profile(driver, device, true) else - device:send(DoorLock.server.commands.LockDoor(device, ep)) + device:send(DoorLock.attributes.LockState:read(device)) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end end -local function handle_unlock(driver, device, command) +local function handle_set_reader_config(driver, device, command) + local cmdName = "setReaderConfig" + local signingKey = command.args.signingKey + local verificationKey = command.args.verificationKey + local groupId = command.args.groupId + local groupResolvingKey = nil + local aliro_ble_uwb_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.ALIROBLEUWB}) + if #aliro_ble_uwb_eps > 0 then + groupResolvingKey = command.args.groupResolvingKey + end + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + local reader_config_updated = device:get_field(lock_utils.ALIRO_READER_CONFIG_UPDATED) or nil + if reader_config_updated == "IN_PROGRESS" then + return + elseif reader_config_updated == "TRUE" then + -- Update commandResult + local command_result_info = { + commandName = cmdName, + statusCode = "success" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.VERIFICATION_KEY, verificationKey, {persist = true}) + device:set_field(lock_utils.GROUP_ID, groupId, {persist = true}) + device:set_field(lock_utils.GROUP_RESOLVING_KEY, groupResolvingKey, {persist = true}) + + -- Send command local ep = device:component_to_endpoint(command.component) - local cota_cred = device:get_field(lock_utils.COTA_CRED) - if cota_cred then - device:send( - DoorLock.server.commands.UnlockDoor(device, ep, cota_cred) + device:send( + DoorLock.server.commands.SetAliroReaderConfig( + device, ep, + lock_utils.hex_string_to_octet_string(signingKey), + lock_utils.hex_string_to_octet_string(verificationKey), + lock_utils.hex_string_to_octet_string(groupId), + lock_utils.hex_string_to_octet_string(groupResolvingKey) ) + ) + device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "IN_PROGRESS", {persist = true}) +end + +local function set_aliro_reader_config_handler(driver, device, ib, response) + -- Get result + local cmdName = device:get_field(lock_utils.COMMAND_NAME) + local status = "failure" + if ib.status == DoorLock.types.DlStatus.SUCCESS then + status = "success" + device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "TRUE", {persist = true}) else - device:send(DoorLock.server.commands.UnlockDoor(device, ep)) + if ib.status == DoorLock.types.DlStatus.INVALID_FIELD then + status = "invalidCommand" + end + device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, nil, {persist = true}) end -end -local function handle_delete_code(driver, device, command) - local endpoint = device:component_to_endpoint(command.component) - device:set_field(lock_utils.DELETING_CODE, command.args.codeSlot) - device:send(DoorLock.server.commands.ClearCredential( - device, - endpoint, - {credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = command.args.codeSlot} + -- Update commandResult + local command_result_info = { + commandName = cmdName, + statusCode = status + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} )) + device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) end -local function handle_reload_all_codes(driver, device, command) - if (device:get_field(lock_utils.CHECKING_CREDENTIAL) == nil) then - lock_utils.lock_codes_event(device, {}) - device:set_field(lock_utils.CHECKING_CREDENTIAL, 1) - else - device.log.info(string.format("Delaying scanning since currently checking credential %d", device:get_field(lock_utils.CHECKING_CREDENTIAL))) - device.thread:call_with_delay(2, function(t) handle_reload_all_codes(driver, device, command) end) - return +local function handle_set_card_id(driver, device, command) + if command.args.cardId ~= nil then + device:emit_event(capabilities.lockAliro.cardId(command.args.cardId, {visibility = {displayed = false}})) end - device.thread:call_with_delay(5, function(t) - device:emit_event(capabilities.lockCodes.scanCodes("Scanning")) - device:send( - clusters.DoorLock.server.commands.GetCredentialStatus( - device, device:component_to_endpoint(command.component), - {credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = device:get_field(lock_utils.CHECKING_CREDENTIAL)} - ) - ) - end) end -local function handle_request_code(driver, device, command) - local endpoint_id = device:component_to_endpoint(command.component) - device:set_field(lock_utils.CHECKING_CREDENTIAL, command.args.codeSlot) +local function handle_set_issuer_key(driver, device, command) + -- Get parameters + local cmdName = "setIssuerKey" + local userIdx = command.args.userIndex + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local issuerKey = command.args.issuerKey + local reqId = command.args.requestId local credential = { - credential_type = DoorLock.types.DlCredentialType.PIN, - credential_index = command.args.codeSlot, + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = INITIAL_CREDENTIAL_INDEX } - device:send(clusters.DoorLock.server.commands.GetCredentialStatus(device, endpoint_id, credential)) -end -local function handle_set_code(driver, device, command) - local endpoint = device:component_to_endpoint(command.component) - if (command.args.codePIN == "") then - driver:inject_capability_command( - device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.nameSlot.NAME, - args = {command.args.codeSlot, command.args.codeName}, - } - ) + -- Adjustment + if userIdx == 0 then + userIdx = nil else - local credential = { - credential_type = DoorLock.types.DlCredentialType.PIN, - credential_index = command.args.codeSlot, + userType = nil + end + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "busy" } - device:set_field(lock_utils.SET_CREDENTIAL, command.args.codeSlot) - device:send( - DoorLock.server.commands.SetCredential( - device, endpoint, DoorLock.types.DlDataOperationType.ADD, -- operation_type - credential, command.args.codePIN, -- credential_data - nil, -- nil user_index creates a new user - DoorLock.types.DlUserStatus.OCCUPIED_ENABLED, DoorLock.types.DlUserType.UNRESTRICTED_USER - ) - ) - if (command.args.codeName ~= nil) then - -- wait for confirmation from the lock to commit this to memory - -- Groovy driver has a lot more info passed here as a description string, may need to be investigated - local codeState = device:get_field(lock_utils.CODE_STATE) or {} - codeState["setName" .. command.args.codeSlot] = command.args.codeName - device:set_field(lock_utils.CODE_STATE, codeState, {persist = true}) - end + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return end -end -local function handle_name_slot(driver, device, command) - local code_slot = tostring(command.args.codeSlot) - local lock_codes = lock_utils.get_lock_codes(device) - if (lock_codes[code_slot] ~= nil) then - lock_codes[code_slot] = command.args.codeName - device:emit_event( - capabilities.lockCodes.codeChanged( - code_slot .. " renamed", {state_change = true} - ) + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.ISSUER_KEY, issuerKey, {persist = true}) + device:set_field(lock_utils.ISSUER_KEY_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + DoorLock.types.DataOperationTypeEnum.ADD, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + lock_utils.hex_string_to_octet_string(issuerKey), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type ) - lock_utils.lock_codes_event(device, lock_codes) - end + ) end -local function find_default_endpoint(device, cluster) - local res = device.MATTER_DEFAULT_ENDPOINT - local eps = device:get_endpoints(cluster) - table.sort(eps) - for _, v in ipairs(eps) do - if v ~= 0 then --0 is the matter RootNode endpoint - return v +local function handle_clear_issuer_key(driver, device, command) + -- Get parameters + local cmdName = "clearIssuerKey" + local userIdx = command.args.userIndex + local reqId = command.args.requestId + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find issuer key index + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyType == "issuerKey" then + -- Set parameters + local credential = { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = entry.keyIndex, + } + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + break end end - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return res end -local function component_to_endpoint(device, component_name) - return find_default_endpoint(device, clusters.DoorLock.ID) -end +local function handle_set_endpoint_key(driver, device, command) + -- Get parameters + local cmdName = "setEndpointKey" + local userIdx = command.args.userIndex + local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER + local keyId = command.args.keyId + local keyType = command.args.keyType + local endpointKey = command.args.endpointKey + local reqId = command.args.requestId + local dataOpType = DoorLock.types.DataOperationTypeEnum.ADD -- Data Operation Type: Add(0), Modify(2) + local endpointKeyIndex = INITIAL_CREDENTIAL_INDEX -local function info_changed(driver, device, event, args) - if device.profile.id ~= args.old_st_store.profile.id then - device:subscribe() + -- Min user index of commandResult is 1 + -- 0 should convert to nil before busy check + if userIdx == 0 then + userIdx = nil end -end -local function do_configure(driver, device) - -- check if the device is NOT currently profiled as base-lock - -- by ANDing a query for every capability in the base-lock profiles. - -- If it does not use base-lock, it is WWST and does not need re-profiling. - if not (device:supports_capability(capabilities.lock) and - device:supports_capability(capabilities.lockCodes) and - device:supports_capability(capabilities.tamperAlert) and - device:supports_capability(capabilities.battery)) then - return - end - - -- if not fingerprinted, dynamically configure base-lock profile - local profile_name = "base-lock" - device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) - local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) - if #battery_feature_eps == 0 then - profile_name = profile_name .. "-nobattery" - device.log.info(string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) - else - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - req:merge(clusters.PowerSource.attributes.AttributeList:read()) - device:send(req) + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return end -end -local function device_init(driver, device) - device:set_component_to_endpoint_fn(component_to_endpoint) - device:subscribe() + -- Adjustment + if userIdx ~= nil then + userType = nil - -- check if we have a missing COTA credential. Only run this if it has not been run before (i.e. in device added), - -- because there is a delay built into the COTA process and we do not want to start two COTA generations at the same time - -- in the event this was triggered on add. - if not device:get_field(lock_utils.COTA_READ_INITIALIZED) or not device:get_field(lock_utils.COTA_CRED) then - local eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.DoorLockFeature.CREDENTIALSOTA | DoorLock.types.DoorLockFeature.PIN_CREDENTIALS}) - if #eps == 0 then - device.log.debug("Device will not require PIN for remote operation") - device:set_field(lock_utils.COTA_CRED, false, {persist = true}) - else - device:send(DoorLock.attributes.RequirePINforRemoteOperation:read(device, eps[1])) - device:set_field(lock_utils.COTA_READ_INITIALIZED, true, {persist = true}) + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + -- Find existing endpoint key + for index, entry in pairs(aliro_table) do + if (entry.keyType == "evictableEndpointKey" or entry.keyType == "nonEvictableEndpointKey") and entry.keyId == keyId then + dataOpType = DoorLock.types.DataOperationTypeEnum.MODIFY + endpointKeyIndex = entry.keyIndex + delete_aliro_from_table(device, userIdx, keyType, keyId) + break + end end end + + local credential = { + credential_type = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType], + credential_index = endpointKeyIndex + } + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.DEVICE_KEY_ID, keyId, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_TYPE, keyType, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY, endpointKey, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_INDEX, endpointKeyIndex, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Send command + local ep = device:component_to_endpoint(command.component) + device:send( + DoorLock.server.commands.SetCredential( + device, ep, + dataOpType, -- Data Operation Type: Add(0), Modify(2) + credential, -- Credential + lock_utils.hex_string_to_octet_string(endpointKey), -- Credential Data + userIdx, -- User Index + nil, -- User Status + userType -- User Type + ) + ) end -local function device_added(driver, device) - --Note: May want to write OperatingMode to NORMAL, to attempt to ensure remote operation works - --Note: May want to write RequirePINForRemoteOperation, to avoid cota cases if possible. - device:emit_event(capabilities.tamperAlert.tamper.clear()) - local eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.DoorLockFeature.PIN_CREDENTIALS}) - if #eps == 0 then - if device:supports_capability_by_id(capabilities.tamperAlert.ID) then - device.log.debug("Device does not support lockCodes. Switching profile.") - local profile_name = "lock-without-codes" - device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) - local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) - if #battery_feature_eps == 0 then - profile_name = profile_name .. "-nobattery" - end - device.log.info(string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) - else - device.log.debug("Device supports neither lock codes nor tamper. Unable to switch profile.") - end +local function handle_clear_endpoint_key(driver, device, command) + -- Get parameters + local cmdName = "clearEndpointKey" + local userIdx = command.args.userIndex + local keyId = command.args.keyId + local keyType = command.args.keyType + local reqId = command.args.requestId + + -- Check busy state + if lock_utils.is_busy_state_set(device) then + local command_result_info = { + commandName = cmdName, + userIndex = userIdx, + keyId = keyId, + requestId = reqId, + statusCode = "busy" + } + device:emit_event(capabilities.lockAliro.commandResult( + command_result_info, {state_change = true, visibility = {displayed = false}} + )) + return + end + + -- Save values to field + device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) + device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) + device:set_field(lock_utils.DEVICE_KEY_ID, keyId, {persist = true}) + device:set_field(lock_utils.ENDPOINT_KEY_TYPE, keyType, {persist = true}) + device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) + + -- Get latest aliro table + local aliro_table = utils.deep_copy(device:get_latest_state( + "main", + capabilities.lockAliro.ID, + capabilities.lockAliro.credentials.NAME, + {} + )) + + local ep = device:component_to_endpoint(command.component) + if keyId == nil then + return else - local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) - req:merge(DoorLock.attributes.MaxPINCodeLength:read(device, eps[1])) - req:merge(DoorLock.attributes.MinPINCodeLength:read(device, eps[1])) - req:merge(DoorLock.attributes.NumberOfPINUsersSupported:read(device, eps[1])) - driver:inject_capability_command(device, { - capability = capabilities.lockCodes.ID, - command = capabilities.lockCodes.commands.reloadAllCodes.NAME, - args = {} - }) - - --Device may require pin for remote operation if it supports COTA and PIN features. - eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.DoorLockFeature.CREDENTIALSOTA | DoorLock.types.DoorLockFeature.PIN_CREDENTIALS}) - if #eps == 0 then - device.log.debug("Device will not require PIN for remote operation") - device:set_field(lock_utils.COTA_CRED, false, {persist = true}) - else - req:merge(DoorLock.attributes.RequirePINforRemoteOperation:read(device, eps[1])) - device:set_field(lock_utils.COTA_READ_INITIALIZED, true, {persist = true}) + -- Find aliro credential + for index, entry in pairs(aliro_table) do + if entry.userIndex == userIdx and entry.keyId == keyId and entry.keyType == keyType then + -- Set parameters + local credential = { + credential_type = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType], + credential_index = entry.keyIndex, + } + -- Send command + device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) + break + end end - device:send(req) end end local matter_lock_driver = { + NAME = "Matter Lock Driver", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + infoChanged = info_changed, + driverSwitched = driver_switched + }, + shared_device_thread_enabled = true, matter_handlers = { attr = { [DoorLock.ID] = { [DoorLock.attributes.LockState.ID] = lock_state_handler, - [DoorLock.attributes.MaxPINCodeLength.ID] = max_pin_code_len_handler, + [DoorLock.attributes.OperatingMode.ID] = operating_modes_handler, + [DoorLock.attributes.NumberOfTotalUsersSupported.ID] = total_users_supported_handler, + [DoorLock.attributes.NumberOfPINUsersSupported.ID] = pin_users_supported_handler, [DoorLock.attributes.MinPINCodeLength.ID] = min_pin_code_len_handler, - [DoorLock.attributes.NumberOfPINUsersSupported.ID] = num_pin_users_handler, + [DoorLock.attributes.MaxPINCodeLength.ID] = max_pin_code_len_handler, [DoorLock.attributes.RequirePINforRemoteOperation.ID] = require_remote_pin_handler, + [DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser.ID] = max_week_schedule_of_user_handler, + [DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser.ID] = max_year_schedule_of_user_handler, + [DoorLock.attributes.AliroReaderVerificationKey.ID] = aliro_reader_verification_key_handler, + [DoorLock.attributes.AliroReaderGroupIdentifier.ID] = aliro_reader_group_id_handler, + [DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions.ID] = aliro_protocol_versions_handler, + [DoorLock.attributes.AliroGroupResolvingKey.ID] = aliro_group_resolving_key_handler, + [DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions.ID] = aliro_supported_ble_uwb_protocol_versions_handler, + [DoorLock.attributes.AliroBLEAdvertisingVersion.ID] = aliro_ble_advertising_version_handler, + [DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported.ID] = max_aliro_credential_issuer_key_handler, + [DoorLock.attributes.NumberOfAliroEndpointKeysSupported.ID] = max_aliro_endpoint_key_handler, + [DoorLockFeatureMapAttr.ID] = door_lock_feature_map_handler, }, [PowerSource.ID] = { [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, [PowerSource.attributes.BatPercentRemaining.ID] = handle_battery_percent_remaining, [PowerSource.attributes.BatChargeLevel.ID] = handle_battery_charge_level, - }, + } }, event = { [DoorLock.ID] = { [DoorLock.events.DoorLockAlarm.ID] = alarm_event_handler, [DoorLock.events.LockOperation.ID] = lock_op_event_handler, - [DoorLock.events.LockUserChange.ID] = lock_user_change_event_handler, }, }, cmd_response = { [DoorLock.ID] = { + [DoorLock.server.commands.SetUser.ID] = set_user_response_handler, + [DoorLock.client.commands.GetUserResponse.ID] = get_user_response_handler, + [DoorLock.server.commands.ClearUser.ID] = clear_user_response_handler, [DoorLock.client.commands.SetCredentialResponse.ID] = set_credential_response_handler, - [DoorLock.client.commands.GetCredentialStatusResponse.ID] = get_credential_status_response_handler, [DoorLock.server.commands.ClearCredential.ID] = clear_credential_response_handler, + [DoorLock.server.commands.SetWeekDaySchedule.ID] = set_week_day_schedule_handler, + [DoorLock.server.commands.ClearWeekDaySchedule.ID] = clear_week_day_schedule_handler, + [DoorLock.server.commands.SetYearDaySchedule.ID] = set_year_day_schedule_handler, + [DoorLock.server.commands.SetAliroReaderConfig.ID] = set_aliro_reader_config_handler, }, }, }, subscribed_attributes = subscribed_attributes, - subscribed_events = { - [capabilities.tamperAlert.ID] = {DoorLock.events.DoorLockAlarm, DoorLock.events.LockOperation}, - [capabilities.lockAlarm.ID] = {DoorLock.events.DoorLockAlarm}, - [capabilities.lockCodes.ID] = {DoorLock.events.LockUserChange}, - }, + subscribed_events = subscribed_events, capability_handlers = { - [capabilities.refresh.ID] = {[capabilities.refresh.commands.refresh.NAME] = handle_refresh}, [capabilities.lock.ID] = { [capabilities.lock.commands.lock.NAME] = handle_lock, [capabilities.lock.commands.unlock.NAME] = handle_unlock, + [capabilities.lock.commands.unlatch.NAME] = handle_unlatch + }, + [capabilities.lockUsers.ID] = { + [capabilities.lockUsers.commands.addUser.NAME] = handle_add_user, + [capabilities.lockUsers.commands.updateUser.NAME] = handle_update_user, + [capabilities.lockUsers.commands.deleteUser.NAME] = handle_delete_user, + [capabilities.lockUsers.commands.deleteAllUsers.NAME] = handle_delete_all_users, + }, + [capabilities.lockCredentials.ID] = { + [capabilities.lockCredentials.commands.addCredential.NAME] = handle_add_credential, + [capabilities.lockCredentials.commands.updateCredential.NAME] = handle_update_credential, + [capabilities.lockCredentials.commands.deleteCredential.NAME] = handle_delete_credential, + [capabilities.lockCredentials.commands.deleteAllCredentials.NAME] = handle_delete_all_credentials, }, - [capabilities.lockCodes.ID] = { - [capabilities.lockCodes.commands.deleteCode.NAME] = handle_delete_code, - [capabilities.lockCodes.commands.reloadAllCodes.NAME] = handle_reload_all_codes, - [capabilities.lockCodes.commands.requestCode.NAME] = handle_request_code, - [capabilities.lockCodes.commands.setCode.NAME] = handle_set_code, - [capabilities.lockCodes.commands.nameSlot.NAME] = handle_name_slot, + [capabilities.lockSchedules.ID] = { + [capabilities.lockSchedules.commands.setWeekDaySchedule.NAME] = handle_set_week_day_schedule, + [capabilities.lockSchedules.commands.clearWeekDaySchedules.NAME] = handle_clear_week_day_schedule, + [capabilities.lockSchedules.commands.setYearDaySchedule.NAME] = handle_set_year_day_schedule, + [capabilities.lockSchedules.commands.clearYearDaySchedules.NAME] = handle_clear_year_day_schedule, }, + [capabilities.lockAliro.ID] = { + [capabilities.lockAliro.commands.setReaderConfig.NAME] = handle_set_reader_config, + [capabilities.lockAliro.commands.setCardId.NAME] = handle_set_card_id, + [capabilities.lockAliro.commands.setIssuerKey.NAME] = handle_set_issuer_key, + [capabilities.lockAliro.commands.clearIssuerKey.NAME] = handle_clear_issuer_key, + [capabilities.lockAliro.commands.setEndpointKey.NAME] = handle_set_endpoint_key, + [capabilities.lockAliro.commands.clearEndpointKey.NAME] = handle_clear_endpoint_key, + }, + [capabilities.refresh.ID] = {[capabilities.refresh.commands.refresh.NAME] = handle_refresh} }, supported_capabilities = { capabilities.lock, - capabilities.lockCodes, - capabilities.tamperAlert, + capabilities.lockUsers, + capabilities.lockCredentials, + capabilities.lockSchedules, capabilities.battery, - capabilities.batteryLevel, + capabilities.batteryLevel }, sub_drivers = require("sub_drivers"), - lifecycle_handlers = { - init = device_init, - added = device_added, - doConfigure = do_configure, - infoChanged = info_changed, - }, - shared_device_thread_enabled = true, } ------------------------------------------------------------------------------------------------------------------------------ --- Driver Initialization ------------------------------------------------------------------------------------------------------------------------------ local matter_driver = MatterDriver("matter-lock", matter_lock_driver) matter_driver:run() diff --git a/drivers/SmartThings/matter-lock/src/lock_utils.lua b/drivers/SmartThings/matter-lock/src/lock_utils.lua index ed804363b6..3836669115 100644 --- a/drivers/SmartThings/matter-lock/src/lock_utils.lua +++ b/drivers/SmartThings/matter-lock/src/lock_utils.lua @@ -46,11 +46,17 @@ local lock_utils = { MODULAR_PROFILE_UPDATED = "__MODULAR_PROFILE_UPDATED", ALIRO_READER_CONFIG_UPDATED = "aliroReaderConfigUpdated", LATEST_DOOR_LOCK_FEATURE_MAP = "latestDoorLockFeatureMap", + LOCK_CODES_COPY_REQUIRED = "lockCodesCopyRequired", + LOCK_CODES_FOR_MIGRATION = "lockCodesForMigration", } local capabilities = require "st.capabilities" local json = require "st.json" local utils = require "st.utils" +lock_utils.MIN_EPOCH_S = 0 +lock_utils.MAX_EPOCH_S = 0xffffffff +lock_utils.THIRTY_YEARS_S = 946684800 -- 1970-01-01T00:00:00 ~ 2000-01-01T00:00:00 + lock_utils.get_lock_codes = function(device) local lc = device:get_field(lock_utils.LOCK_CODES) return lc ~= nil and lc or {} @@ -170,6 +176,34 @@ function lock_utils.is_busy_state_set(device) end end +-- This type represents an offset, in seconds, from 0 hours, 0 minutes, 0 seconds, on the 1st of January, 2000 UTC +function lock_utils.iso8601_to_epoch(iso_str) + local pattern = "^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)" + local year, month, day, hour, min, sec = iso_str:match(pattern) + if not year then + return nil + end + local epoch_s = os.time({ + year = tonumber(year), + month = tonumber(month), + day = tonumber(day), + hour = tonumber(hour), + min = tonumber(min), + sec = tonumber(sec), + }) + + -- The os.time() is based on 1970. Thirty years must be subtracted for calculations from 2000. + epoch_s = epoch_s - lock_utils.THIRTY_YEARS_S + + if epoch_s < lock_utils.MIN_EPOCH_S then + return lock_utils.MIN_EPOCH_S + elseif epoch_s > lock_utils.MAX_EPOCH_S then + return lock_utils.MAX_EPOCH_S + else + return epoch_s + end +end + function lock_utils.hex_string_to_octet_string(hex_string) if hex_string == nil then return nil diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua deleted file mode 100644 index 7bbb45ed44..0000000000 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/can_handle.lua +++ /dev/null @@ -1,19 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function is_new_matter_lock_products(opts, driver, device) - local device_lib = require "st.device" - if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then - return false - end - local FINGERPRINTS = require("new-matter-lock.fingerprints") - for _, p in ipairs(FINGERPRINTS) do - if device.manufacturer_info.vendor_id == p[1] and - device.manufacturer_info.product_id == p[2] then - return true, require("new-matter-lock") - end - end - return false -end - -return is_new_matter_lock_products diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua deleted file mode 100644 index fef7746990..0000000000 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/fingerprints.lua +++ /dev/null @@ -1,50 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local NEW_MATTER_LOCK_PRODUCTS = { - {0x115f, 0x2802}, -- AQARA, U200 - {0x115f, 0x2801}, -- AQARA, U300 - {0x115f, 0x2807}, -- AQARA, U200 Lite - {0x115f, 0x2804}, -- AQARA, U400 - {0x115f, 0x286A}, -- AQARA, U200 US - {0x115f, 0x2805}, -- Aqara Smart Lock J200 Set - {0x115f, 0x280e}, -- AQARA Smart Gate Lock U500 - {0x115F, 0x280F}, -- Aqara Smart Lock U500 Rim - {0x115F, 0x2810}, -- Aqara Smart Lock U500 Glass Door - {0x115F, 0x284A}, -- Aqara Smart Lock U600 - {0x115F, 0x280D}, -- Aqara Smart Lock J200 - {0x147F, 0x0001}, -- U-tec - {0x147F, 0x0007}, -- ULTRALOQ Bolt Pro Smart Matter Door Lock - {0x147F, 0x0008}, -- Ultraloq, Bolt Smart Matter Door Lock - {0x147F, 0x0010}, -- ULTRALOQ Latch 5 Pro Smart Matter Door Lock - {0x144F, 0x4002}, -- Yale, Linus Smart Lock L2 - {0x101D, 0x8110}, -- Yale, New Lock - {0x1533, 0x0001}, -- eufy, E31 - {0x1533, 0x0002}, -- eufy, E30 - {0x1533, 0x0003}, -- eufy, C34 - {0x1533, 0x000F}, -- eufy, FamiLock S3 Max - {0x1533, 0x0010}, -- eufy, FamiLock S3 - {0x1533, 0x0011}, -- eufy, FamiLock E34 - {0x1533, 0x0012}, -- eufy, FamiLock E35 - {0x1533, 0x0016}, -- eufy, FamiLock E32 - {0x1533, 0x0014}, -- eufy, FamiLock E40 - {0x1533, 0x001F}, -- eufy, EAP - {0x135D, 0x00B1}, -- Nuki, Smart Lock Pro - {0x135D, 0x00B2}, -- Nuki, Smart Lock - {0x135D, 0x00C1}, -- Nuki, Smart Lock - {0x135D, 0x00A1}, -- Nuki, Smart Lock - {0x135D, 0x00B0}, -- Nuki, Smart Lock - {0x15F2, 0x0001}, -- Viomi, AiSafety Smart Lock E100 - {0x158B, 0x0001}, -- Deasino, DS-MT01 - {0x10E1, 0x2002}, -- VDA - {0x1421, 0x0041}, -- Kwikset Halo Select Plus - {0x1421, 0x0042}, -- Kwikset Halo Select Plus - {0x1421, 0x0081}, -- Kwikset Aura Reach - {0x1236, 0xa538}, -- Schlage Sense Pro - {0x1236, 0x3800}, -- Schlage - {0x1236, 0xA738}, -- Schlage - {0x152C, 0x9500}, -- Dreame NAVO Smart Lock A10 - {0x152C, 0x9501} -- Dreame NAVO Smart Lock E10 -} - -return NEW_MATTER_LOCK_PRODUCTS diff --git a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua deleted file mode 100644 index 3850d71380..0000000000 --- a/drivers/SmartThings/matter-lock/src/new-matter-lock/init.lua +++ /dev/null @@ -1,3049 +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 im = require "st.matter.interaction_model" -local utils = require "st.utils" -local lock_utils = require "lock_utils" - -local version = require "version" -if version.api < 10 then - clusters.DoorLock = require "DoorLock" -end - -local DoorLock = clusters.DoorLock -local PowerSource = clusters.PowerSource - -local INITIAL_CREDENTIAL_INDEX = 1 -local ALL_INDEX = 0xFFFE --- maximum as defined by the Matter specification -local MAX_USER_NAME_LENGTH = 10 -local MIN_EPOCH_S = 0 -local MAX_EPOCH_S = 0xffffffff -local THIRTY_YEARS_S = 946684800 -- 1970-01-01T00:00:00 ~ 2000-01-01T00:00:00 - -local RESPONSE_STATUS_MAP = { - [DoorLock.types.DlStatus.SUCCESS] = "success", - [DoorLock.types.DlStatus.FAILURE] = "failure", - [DoorLock.types.DlStatus.DUPLICATE] = "duplicate", - [DoorLock.types.DlStatus.OCCUPIED] = "occupied", - [DoorLock.types.DlStatus.INVALID_FIELD] = "invalidCommand", - [DoorLock.types.DlStatus.RESOURCE_EXHAUSTED] = "resourceExhausted", - [DoorLock.types.DlStatus.NOT_FOUND] = "failure" -} - -local WEEK_DAY_MAP = { - ["Sunday"] = 1, - ["Monday"] = 2, - ["Tuesday"] = 4, - ["Wednesday"] = 8, - ["Thursday"] = 16, - ["Friday"] = 32, - ["Saturday"] = 64, -} - -local ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP = { - ["evictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_EVICTABLE_ENDPOINT_KEY, - ["nonEvictableEndpointKey"] = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY -} - -local battery_support = { - NO_BATTERY = "NO_BATTERY", - BATTERY_LEVEL = "BATTERY_LEVEL", - BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" -} - -local profiling_data = { - BATTERY_SUPPORT = "__BATTERY_SUPPORT", -} - -local DoorLockFeatureMapAttr = {ID = 0xFFFC, cluster = DoorLock.ID} -local subscribed_attributes = { - [capabilities.lock.ID] = { - DoorLock.attributes.LockState - }, - [capabilities.remoteControlStatus.ID] = { - DoorLock.attributes.OperatingMode - }, - [capabilities.lockUsers.ID] = { - DoorLock.attributes.NumberOfTotalUsersSupported - }, - [capabilities.lockCredentials.ID] = { - DoorLock.attributes.NumberOfPINUsersSupported, - DoorLock.attributes.MaxPINCodeLength, - DoorLock.attributes.MinPINCodeLength, - DoorLock.attributes.RequirePINforRemoteOperation - }, - [capabilities.lockSchedules.ID] = { - DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser, - DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser - }, - [capabilities.lockAliro.ID] = { - DoorLock.attributes.AliroReaderVerificationKey, - DoorLock.attributes.AliroReaderGroupIdentifier, - DoorLock.attributes.AliroReaderGroupSubIdentifier, - DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions, - DoorLock.attributes.AliroGroupResolvingKey, - DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions, - DoorLock.attributes.AliroBLEAdvertisingVersion, - DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported, - DoorLock.attributes.NumberOfAliroEndpointKeysSupported, - }, - [capabilities.battery.ID] = { - PowerSource.attributes.BatPercentRemaining - }, - [capabilities.batteryLevel.ID] = { - PowerSource.attributes.BatChargeLevel - } -} - -local subscribed_events = { - [capabilities.lock.ID] = { - DoorLock.events.LockOperation - }, - [capabilities.lockAlarm.ID] = { - DoorLock.events.DoorLockAlarm - }, - [capabilities.lockUsers.ID] = { - DoorLock.events.LockUserChange - } -} - -local function find_default_endpoint(device, cluster) - local res = device.MATTER_DEFAULT_ENDPOINT - local eps = device:get_endpoints(cluster) - table.sort(eps) - for _, v in ipairs(eps) do - if v ~= 0 then --0 is the matter RootNode endpoint - return v - end - end - device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) - return res -end - -local function component_to_endpoint(device, component_name) - return find_default_endpoint(device, clusters.DoorLock.ID) -end - -local function device_init(driver, device) - device:set_component_to_endpoint_fn(component_to_endpoint) - if #device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) == 0 then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, {persist = true}) - elseif device:get_field(profiling_data.BATTERY_SUPPORT) == nil then - device:add_subscribed_attribute(clusters.PowerSource.attributes.AttributeList) - end - for cap_id, attributes in pairs(subscribed_attributes) do - if device:supports_capability_by_id(cap_id) then - for _, attr in ipairs(attributes) do - device:add_subscribed_attribute(attr) - end - end - end - device:add_subscribed_attribute(DoorLockFeatureMapAttr) - for cap_id, events in pairs(subscribed_events) do - if device:supports_capability_by_id(cap_id) then - for _, e in ipairs(events) do - device:add_subscribed_event(e) - end - end - end - device:subscribe() - end - -local function device_added(driver, device) - device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) -end - -local function set_reader_config(device) - local reader_config_updated = device:get_field(lock_utils.ALIRO_READER_CONFIG_UPDATED) or nil - if reader_config_updated == "TRUE" or reader_config_updated == "IN_PROGRESS" then return end - - local cmdName = "setReaderConfig" - local groupId = lock_utils.create_group_id_resolving_key() - local groupResolvingKey = nil - local aliro_ble_uwb_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.ALIROBLEUWB}) - if #aliro_ble_uwb_eps > 0 then - groupResolvingKey = lock_utils.create_group_id_resolving_key() - end - local privKey, pubKey = lock_utils.generate_keypair(device) - if not privKey or not pubKey then - local command_result_info = { - commandName = cmdName, - statusCode = "failure" - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.VERIFICATION_KEY, pubKey, {persist = true}) - device:set_field(lock_utils.GROUP_ID, groupId, {persist = true}) - device:set_field(lock_utils.GROUP_RESOLVING_KEY, groupResolvingKey, {persist = true}) - - -- Send command - local ep = find_default_endpoint(device, clusters.DoorLock.ID) - device:send( - DoorLock.server.commands.SetAliroReaderConfig( - device, ep, - lock_utils.hex_string_to_octet_string(privKey), - lock_utils.hex_string_to_octet_string(pubKey), - lock_utils.hex_string_to_octet_string(groupId), - lock_utils.hex_string_to_octet_string(groupResolvingKey) - ) - ) - device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "IN_PROGRESS", {persist = true}) -end - -local function match_profile_modular(driver, device) - local enabled_optional_component_capability_pairs = {} - local main_component_capabilities = {} - local modular_profile_name = "lock-modular" - for _, device_ep in pairs(device.endpoints) do - for _, ep_cluster in pairs(device_ep.clusters) do - if ep_cluster.cluster_id == DoorLock.ID then - local clus_has_feature = function(feature_bitmap) - return DoorLock.are_features_supported( - feature_bitmap, - lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, device_ep.endpoint_id) or - ep_cluster.feature_map - ) - end - if clus_has_feature(DoorLock.types.Feature.USER) then - table.insert(main_component_capabilities, capabilities.lockUsers.ID) - end - if clus_has_feature(DoorLock.types.Feature.PIN_CREDENTIAL) then - table.insert(main_component_capabilities, capabilities.lockCredentials.ID) - end - if clus_has_feature(DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES) or - clus_has_feature(DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES) then - table.insert(main_component_capabilities, capabilities.lockSchedules.ID) - end - if clus_has_feature(DoorLock.types.Feature.UNBOLT) then - device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "unlatched", "not fully locked"}, {visibility = {displayed = false}})) - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - modular_profile_name = "lock-modular-embedded-unlatch" -- use the embedded config specified in this profile for devices supporting "unlatch" - else - device:emit_event(capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - end - if clus_has_feature(DoorLock.types.Feature.ALIRO_PROVISIONING) then - table.insert(main_component_capabilities, capabilities.lockAliro.ID) - end - break - end - end - end - - local supported_battery_type = device:get_field(profiling_data.BATTERY_SUPPORT) - if supported_battery_type == battery_support.BATTERY_LEVEL then - table.insert(main_component_capabilities, capabilities.batteryLevel.ID) - elseif supported_battery_type == battery_support.BATTERY_PERCENTAGE then - table.insert(main_component_capabilities, capabilities.battery.ID) - end - - table.insert(enabled_optional_component_capability_pairs, {"main", main_component_capabilities}) - if modular_profile_name == "lock-modular-embedded-unlatch" -- the embedded config that may be needed is not checked by an optional capability comparison - or lock_utils.optional_capabilities_list_changed(enabled_optional_component_capability_pairs, device.profile.components) then - device:try_update_metadata({profile = modular_profile_name, optional_component_capabilities = enabled_optional_component_capability_pairs}) - end -end - -local function match_profile_switch(driver, device) - local user_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.USER}) - local pin_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.PIN_CREDENTIAL}) - local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) - local year_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES}) - local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) - - local profile_name = "lock" - if #user_eps > 0 then - profile_name = profile_name .. "-user" - if #pin_eps > 0 then - profile_name = profile_name .. "-pin" - end - if #week_schedule_eps + #year_schedule_eps > 0 then - profile_name = profile_name .. "-schedule" - end - end - if #unbolt_eps > 0 then - profile_name = profile_name .. "-unlatch" - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - else - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - end - - local supported_battery_type = device:get_field(profiling_data.BATTERY_SUPPORT) - if supported_battery_type == battery_support.BATTERY_LEVEL then - profile_name = profile_name .. "-batteryLevel" - elseif supported_battery_type == battery_support.BATTERY_PERCENTAGE then - profile_name = profile_name .. "-battery" - end - - device.log.info_with({hub_logs=true}, string.format("Updating device profile to %s.", profile_name)) - device:try_update_metadata({profile = profile_name}) -end - ---- Deeply compare two values. ---- Handles metatables. Can optionally ignore cycle checking and/or function differences. ---- ---- @param a any ---- @param b any ---- @param opts table|nil { ignore_functions = boolean, ignore_cycles = boolean } ---- @param seen table|nil ---- @return boolean -local function deep_equals(a, b, opts, seen) - if a == b then return true end -- same object - if type(a) ~= type(b) then return false end -- different type - if type(a) == "function" and opts and opts.ignore_functions then return true end - if type(a) ~= "table" then return false end -- same type but not table, thus was already compared - - -- check for cycles in table references and preserve reference topology. - if not (opts and opts.ignore_cycles) then - seen = seen or {} - seen[a] = seen[a] or {} - if seen[a][b] then - return seen[a][b] - end - seen[a][b] = true - end - - -- Compare keys/values from a - for k, v in pairs(a) do - if not deep_equals(v, b[k], opts, seen) then - return false - end - end - - -- Ensure b doesn't have extra keys - for k in pairs(b) do - if a[k] == nil then - return false - end - end - - -- Compare metatables - local mt_a = getmetatable(a) - local mt_b = getmetatable(b) - return deep_equals(mt_a, mt_b, opts, seen) -end - -local function info_changed(driver, device, event, args) - if deep_equals(device.profile, args.old_st_store.profile, { ignore_functions = true }) then - return - end - for cap_id, attributes in pairs(subscribed_attributes) do - if device:supports_capability_by_id(cap_id) then - for _, attr in ipairs(attributes) do - device:add_subscribed_attribute(attr) - end - end - end - for cap_id, events in pairs(subscribed_events) do - if device:supports_capability_by_id(cap_id) then - for _, e in ipairs(events) do - device:add_subscribed_event(e) - end - end - end - device:subscribe() - if device:supports_capability_by_id(capabilities.lockAliro.ID) then - set_reader_config(device) - end - if device:get_latest_state("main", capabilities.lockAlarm.ID, capabilities.lockAlarm.supportedAlarmValues.NAME) == nil then - device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) - device:emit_event(capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) -- lockJammed is mandatory - end -end - -local function profiling_data_still_required(device) - for _, field in pairs(profiling_data) do - if device:get_field(field) == nil then - return true -- data still required if a field is nil - end - end - return false -end - -local function match_profile(driver, device, ignore_static_profiling) - if profiling_data_still_required(device) then return end - if version.api >= 15 and version.rpc >= 9 then - match_profile_modular(driver, device) - elseif ignore_static_profiling ~= true then - match_profile_switch(driver, device) - end -end - -local function do_configure(driver, device) - match_profile(driver, device, false) - device.thread:call_with_delay(5, function() - device:emit_event(capabilities.lockAlarm.alarm.clear({state_change = true})) - device:emit_event(capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) -- lockJammed is mandatory - end) -end - -local function driver_switched(driver, device) - match_profile(driver, device, false) - device:try_update_metadata({provisioning_state = "PROVISIONED"}) -end - --- Matter Handler ----------------- --- Lock State -- ----------------- -local function lock_state_handler(driver, device, ib, response) - local LockState = DoorLock.attributes.LockState - local attr = capabilities.lock.lock - local LOCK_STATE = { - [LockState.NOT_FULLY_LOCKED] = attr.not_fully_locked(), - [LockState.LOCKED] = attr.locked(), - [LockState.UNLOCKED] = attr.unlocked(), - [LockState.UNLATCHED] = attr.unlatched() - } - - -- The lock state is usually updated in lock_state_handler and lock_op_event_handler, respectively. - -- In this case, two events occur. To prevent this, when both functions are called, - -- it send the event after 1 second so that no event occurs in the lock_state_handler. - device.thread:call_with_delay(1, function () - if ib.data.value ~= nil then - device:emit_event(LOCK_STATE[ib.data.value]) - else - device.log.warn("Lock State is nil") - end - end) -end - ---------------------- --- Operating Modes -- ---------------------- -local function operating_modes_handler(driver, device, ib, response) - local status = capabilities.remoteControlStatus.remoteControlEnabled - local op_type = DoorLock.types.OperatingModeEnum - local opMode_map = { - [op_type.NORMAL] = true, - [op_type.VACATION] = true, - [op_type.PRIVACY] = false, - [op_type.NO_REMOTE_LOCK_UNLOCK] = false, - [op_type.PASSAGE] = false, - } - local result = opMode_map[ib.data.value] - local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) - if result == true then - device:emit_event(status("true", {visibility = {displayed = true}})) - if #unbolt_eps > 0 then - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock", "unlatch"}, {visibility = {displayed = false}})) - else - device:emit_event(capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - end - elseif result == false then - device:emit_event(status("false", {visibility = {displayed = true}})) - device:emit_event(capabilities.lock.supportedLockCommands({}, {visibility = {displayed = false}})) - end -end - -------------------------------------- --- Number Of Total Users Supported -- -------------------------------------- -local function total_users_supported_handler(driver, device, ib, response) - device:emit_event(capabilities.lockUsers.totalUsersSupported(ib.data.value, {visibility = {displayed = false}})) -end - ----------------------------------- --- Number Of PIN Users Supported -- ----------------------------------- -local function pin_users_supported_handler(driver, device, ib, response) - device:emit_event(capabilities.lockCredentials.pinUsersSupported(ib.data.value, {visibility = {displayed = false}})) -end - -------------------------- --- Min PIN Code Length -- -------------------------- -local function min_pin_code_len_handler(driver, device, ib, response) - device:emit_event(capabilities.lockCredentials.minPinCodeLen(ib.data.value, {visibility = {displayed = false}})) -end - -------------------------- --- Max PIN Code Length -- -------------------------- -local function max_pin_code_len_handler(driver, device, ib, response) - device:emit_event(capabilities.lockCredentials.maxPinCodeLen(ib.data.value, {visibility = {displayed = false}})) -end - --------------------------------------- --- Require PIN For Remote Operation -- --------------------------------------- -local function set_cota_credential(device, credential_index) - local eps = device:get_endpoints(DoorLock.ID) - local cota_cred = device:get_field(lock_utils.COTA_CRED) - if cota_cred == nil then - -- Shouldn't happen but defensive to try to figure out if we need the cota cred and set it. - device:send(DoorLock.attributes.RequirePINforRemoteOperation:read(device, #eps > 0 and eps[1] or 1)) - return - elseif cota_cred == false then - device.log.debug("Device does not require PIN for remote operation. Not setting COTA credential") - return - end - - -- Check Busy State - if lock_utils.is_busy_state_set(device) then - device.log.debug("delaying setting COTA credential since a credential is currently being set") - device.thread:call_with_delay(2, function(t) - set_cota_credential(device, credential_index) - end) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, "addCota") - device:set_field(lock_utils.CRED_INDEX, credential_index) - device:set_field(lock_utils.COTA_CRED_INDEX, credential_index, {persist = true}) - device:set_field(lock_utils.USER_TYPE, "remote") - - -- Send command - device.log.info(string.format("Attempting to set COTA credential at index %s", credential_index)) - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = credential_index - } - device:send(DoorLock.server.commands.SetCredential( - device, - #eps > 0 and eps[1] or 1, - DoorLock.types.DataOperationTypeEnum.ADD, - credential, - device:get_field(lock_utils.COTA_CRED), - nil, -- nil user_index creates a new user - DoorLock.types.UserStatusEnum.OCCUPIED_ENABLED, - DoorLock.types.UserTypeEnum.REMOTE_ONLY_USER - )) -end - -local function generate_cota_cred_for_device(device) - local len = device:get_latest_state("main", capabilities.lockCredentials.ID, capabilities.lockCredentials.maxPinCodeLen.NAME) or 6 - local cred_data = math.floor(math.random() * (10 ^ len)) - cred_data = string.format("%0" .. tostring(len) .. "d", cred_data) - device:set_field(lock_utils.COTA_CRED, cred_data, {persist = true}) -end - -local function apply_cota_credentials_if_absent(device) - if not device:get_field(lock_utils.COTA_CRED) then - -- Process after all other info blocks have been dispatched to ensure MaxPINCodeLength has been processed - device.thread:call_with_delay(0, function(t) - generate_cota_cred_for_device(device) - -- delay needed to allow test to override the random credential data - device.thread:call_with_delay(0, function(t) - -- Attempt to set cota credential at the lowest index - set_cota_credential(device, INITIAL_CREDENTIAL_INDEX) - end) - end) - end -end - -local function require_remote_pin_handler(driver, device, ib, response) - if ib.data.value then - apply_cota_credentials_if_absent(device) - else - device:set_field(lock_utils.COTA_CRED, false, {persist = true}) - end -end - ------------------------------------------------------ --- Number Of Week Day Schedules Supported Per User -- ------------------------------------------------------ -local function max_week_schedule_of_user_handler(driver, device, ib, response) - device:emit_event(capabilities.lockSchedules.weekDaySchedulesPerUser(ib.data.value, {visibility = {displayed = false}})) -end - ------------------------------------------------------ --- Number Of Year Day Schedules Supported Per User -- ------------------------------------------------------ -local function max_year_schedule_of_user_handler(driver, device, ib, response) - device:emit_event(capabilities.lockSchedules.yearDaySchedulesPerUser(ib.data.value, {visibility = {displayed = false}})) -end - ------------------------------------ --- Aliro Reader Verification Key -- ------------------------------------ -local function aliro_reader_verification_key_handler(driver, device, ib, response) - if ib.data.value ~= nil then - device:emit_event(capabilities.lockAliro.readerVerificationKey( - utils.bytes_to_hex_string(ib.data.value), {visibility = {displayed = false}} - )) - device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "TRUE", {persist = true}) - end -end - ------------------------------------ --- Aliro Reader Group Identifier -- ------------------------------------ -local function aliro_reader_group_id_handler(driver, device, ib, response) - if ib.data.value ~= nil then - device:emit_event(capabilities.lockAliro.readerGroupIdentifier( - utils.bytes_to_hex_string(ib.data.value), - {visibility = {displayed = false}} - )) - end -end - -------------------------------------------------------------- --- Aliro Expedited Transaction Supported Protocol Versions -- -------------------------------------------------------------- -local function aliro_group_resolving_key_handler(driver, device, ib, response) - if ib.data.value ~= nil then - device:emit_event(capabilities.lockAliro.groupResolvingKey( - utils.bytes_to_hex_string(ib.data.value), - {visibility = {displayed = false}} - )) - end -end - -------------------------------- --- Aliro Group Resolving Key -- -------------------------------- -local function aliro_protocol_versions_handler(driver, device, ib, response) - if ib.data.elements == nil then - return - end - local protocol_versions = {} - for i, element in ipairs(ib.data.elements) do - local version = string.format("%s.%s", element.value:byte(1), element.value:byte(2)) - table.insert(protocol_versions, version); - end - device:emit_event(capabilities.lockAliro.expeditedTransactionProtocolVersions(protocol_versions, {visibility = {displayed = false}})) -end - ------------------------------------------------ --- Aliro Supported BLE UWB Protocol Versions -- ------------------------------------------------ -local function aliro_supported_ble_uwb_protocol_versions_handler(driver, device, ib, response) - if ib.data.elements == nil then - return - end - local protocol_versions = {} - for i, element in ipairs(ib.data.elements) do - local version = string.format("%s.%s", element.value:byte(1), element.value:byte(2)) - table.insert(protocol_versions, version); - end - device:emit_event(capabilities.lockAliro.bleUWBProtocolVersions(protocol_versions, {visibility = {displayed = false}})) -end - ------------------------------------ --- Aliro BLE Advertising Version -- ------------------------------------ -local function aliro_ble_advertising_version_handler(driver, device, ib, response) - if ib.data.value ~= nil then - device:emit_event(capabilities.lockAliro.bleAdvertisingVersion(string.format("%s", ib.data.value), {visibility = {displayed = false}})) - end -end - ------------------------------------------------------- --- Number Of Aliro Credential Issuer Keys Supported -- ------------------------------------------------------- -local function max_aliro_credential_issuer_key_handler(driver, device, ib, response) - if ib.data.value ~= nil then - device:emit_event(capabilities.lockAliro.maxCredentialIssuerKeys(ib.data.value, {visibility = {displayed = false}})) - end -end - ---------------------------------------------- --- Number Of Aliro Endpoint Keys Supported -- ---------------------------------------------- -local function max_aliro_endpoint_key_handler(driver, device, ib, response) - if ib.data.value ~= nil then - device:emit_event(capabilities.lockAliro.maxEndpointKeys(ib.data.value, {visibility = {displayed = false}})) - end -end - ------------------------------- --- Feature Map of Door Lock -- ------------------------------- -local function door_lock_feature_map_handler(driver, device, ib, response) - if ib.data.value == nil then return end - local feature_map = lock_utils.get_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id) or nil - if feature_map ~= ib.data.value then - lock_utils.set_field_for_endpoint(device, lock_utils.LATEST_DOOR_LOCK_FEATURE_MAP, ib.endpoint_id, ib.data.value, { persist = true }) - match_profile(driver, device, true) - end -end - ---------------------------------- --- Power Source Attribute List -- ---------------------------------- -local function handle_power_source_attribute_list(driver, device, ib, response) - local latest_battery_support = device:get_field(profiling_data.BATTERY_SUPPORT) - for _, attr in ipairs(ib.data.elements or {}) do - if attr.value == clusters.PowerSource.attributes.BatPercentRemaining.ID then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE, {persist=true}) - break -- BATTERY_PERCENTAGE is highest priority. break early if found - elseif attr.value == clusters.PowerSource.attributes.BatChargeLevel.ID then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_LEVEL, {persist=true}) - end - end - -- in the case that 1) no battery has been set, and 2) the returned ib does not include battery attributes, ignore battery - if latest_battery_support == nil and not device:get_field(profiling_data.BATTERY_SUPPORT) then - device:set_field(profiling_data.BATTERY_SUPPORT, battery_support.NO_BATTERY, {persist=true}) - end - if latest_battery_support == nil or latest_battery_support ~= device:get_field(profiling_data.BATTERY_SUPPORT) then - match_profile(driver, device, false) - end -end - -------------------------------- --- Battery Percent Remaining -- -------------------------------- -local function handle_battery_percent_remaining(driver, device, ib, response) - if ib.data.value ~= nil then - device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) - end -end - --------------------------- --- Battery Charge Level -- --------------------------- -local function handle_battery_charge_level(driver, device, ib, response) - if ib.data.value == PowerSource.types.BatChargeLevelEnum.OK then - device:emit_event(capabilities.batteryLevel.battery.normal()) - elseif ib.data.value == PowerSource.types.BatChargeLevelEnum.WARNING then - device:emit_event(capabilities.batteryLevel.battery.warning()) - elseif ib.data.value == PowerSource.types.BatChargeLevelEnum.CRITICAL then - device:emit_event(capabilities.batteryLevel.battery.critical()) - end -end - --- Capability Handler ------------------ --- Lock/Unlock -- ------------------ -local function handle_lock(driver, device, command) - local ep = device:component_to_endpoint(command.component) - local cota_cred = device:get_field(lock_utils.COTA_CRED) - if cota_cred then - device:send( - DoorLock.server.commands.LockDoor(device, ep, cota_cred) - ) - else - device:send(DoorLock.server.commands.LockDoor(device, ep)) - end -end - -local function handle_unlock(driver, device, command) - local unbolt_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.UNBOLT}) - local cota_cred = device:get_field(lock_utils.COTA_CRED) - local ep = device:component_to_endpoint(command.component) - - if #unbolt_eps > 0 then - if cota_cred then - device:send( - DoorLock.server.commands.UnboltDoor(device, ep, cota_cred) - ) - else - device:send(DoorLock.server.commands.UnboltDoor(device, ep)) - end - else - if cota_cred then - device:send( - DoorLock.server.commands.UnlockDoor(device, ep, cota_cred) - ) - else - device:send(DoorLock.server.commands.UnlockDoor(device, ep)) - end - end -end - -local function handle_unlatch(driver, device, command) - local ep = device:component_to_endpoint(command.component) - local cota_cred = device:get_field(lock_utils.COTA_CRED) - if cota_cred then - device:send( - DoorLock.server.commands.UnlockDoor(device, ep, cota_cred) - ) - else - device:send(DoorLock.server.commands.UnlockDoor(device, ep)) - end -end - ----------------- --- User Table -- ----------------- -local function add_user_to_table(device, userIdx, userName, userType) - -- Get latest user table - local user_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockUsers.ID, - capabilities.lockUsers.users.NAME, - {} - )) - - -- Add new entry to table - table.insert(user_table, {userIndex = userIdx, userName = userName, userType = userType}) - device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) -end - -local function update_user_in_table(device, userIdx, userName, userType) - -- Get latest user table - local user_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockUsers.ID, - capabilities.lockUsers.users.NAME, - {} - )) - - -- Find user entry - local i = 0 - for index, entry in pairs(user_table) do - if entry.userIndex == userIdx then - i = index - break - end - end - - -- Update user entry - if i ~= 0 then - user_table[i].userType = userType - user_table[i].userName = userName - device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) - end -end - -local function delete_user_from_table(device, userIdx) - -- If User Index is ALL_INDEX, remove all entry from the table - if userIdx == ALL_INDEX then - device:emit_event(capabilities.lockUsers.users({}, {visibility = {displayed = false}})) - return - end - - -- Get latest user table - local user_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockUsers.ID, - capabilities.lockUsers.users.NAME, - {} - )) - - -- Remove element from user table - for index, entry in pairs(user_table) do - if entry.userIndex == userIdx then - table.remove(user_table, index) - break - end - end - device:emit_event(capabilities.lockUsers.users(user_table, {visibility = {displayed = false}})) -end - ----------------------- --- Credential Table -- ----------------------- -local function has_credentials(device, userIdx) - -- Get latest credential table - local cred_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockCredentials.ID, - capabilities.lockCredentials.credentials.NAME, - {} - )) - - -- Find credential - for index, entry in pairs(cred_table) do - if entry.userIndex == userIdx then - return true - end - end - - -- Get latest Aliro credential table - local aliro_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockAliro.ID, - capabilities.lockAliro.credentials.NAME, - {} - )) - - -- Find Aliro credential - for index, entry in pairs(aliro_table) do - if entry.userIndex == userIdx then - return true - end - end - - return false -end - -local function add_credential_to_table(device, userIdx, credIdx, credType) - -- Get latest credential table - local cred_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockCredentials.ID, - capabilities.lockCredentials.credentials.NAME, - {} - )) - - -- Add new entry to table - table.insert(cred_table, {userIndex = userIdx, credentialIndex = credIdx, credentialType = credType}) - device:emit_event(capabilities.lockCredentials.credentials(cred_table, {visibility = {displayed = false}})) -end - -local function delete_credential_from_table(device, credIdx) - -- If Credential Index is ALL_INDEX, remove all entries from the table - if credIdx == ALL_INDEX then - device:emit_event(capabilities.lockCredentials.credentials({}, {visibility = {displayed = false}})) - return ALL_INDEX - end - - -- Get latest credential table - local cred_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockCredentials.ID, - capabilities.lockCredentials.credentials.NAME, - {} - )) - - -- Delete an entry from credential table - local userIdx = nil - for index, entry in pairs(cred_table) do - if entry.credentialIndex == credIdx then - table.remove(cred_table, index) - userIdx = entry.userIndex - break - end - end - - device:emit_event(capabilities.lockCredentials.credentials(cred_table, {visibility = {displayed = false}})) - return userIdx -end - -local function delete_credential_from_table_as_user(device, userIdx) - -- If User Index is ALL_INDEX, remove all entry from the table - if userIdx == ALL_INDEX then - device:emit_event(capabilities.lockCredentials.credentials({}, {visibility = {displayed = false}})) - return - end - - -- Get latest credential table - local cred_table = device:get_latest_state( - "main", - capabilities.lockCredentials.ID, - capabilities.lockCredentials.credentials.NAME - ) or {} - local new_cred_table = {} - - -- Re-create credential table - for index, entry in pairs(cred_table) do - if entry.userIndex ~= userIdx then - table.insert(new_cred_table, entry) - end - end - - device:emit_event(capabilities.lockCredentials.credentials(new_cred_table, {visibility = {displayed = false}})) -end - ------------------------------ --- Week Day Schedule Table -- ------------------------------ -local function add_week_schedule_to_table(device, userIdx, scheduleIdx, schedule) - -- Get latest week day schedule table - local week_schedule_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockSchedules.ID, - capabilities.lockSchedules.weekDaySchedules.NAME, - {} - )) - - -- Find schedule for specific user - local i = 0 - for index, entry in pairs(week_schedule_table) do - if entry.userIndex == userIdx then - i = index - end - end - - -- Re-create weekDays list - local weekDayList = {} - for _, weekday in ipairs(schedule.weekDays) do - table.insert(weekDayList, weekday) - end - - if i ~= 0 then -- Add schedule for existing user - -- Exclude same scheduleIdx - local new_schedule_table = {} - for index, entry in pairs(week_schedule_table[i].schedules) do - if entry.scheduleIndex ~= scheduleIdx then - table.insert(new_schedule_table, entry) - end - end - -- Add new entry to table - table.insert( - new_schedule_table, - { - scheduleIndex = scheduleIdx, - weekDays = weekDayList, - startHour = schedule.startHour, - startMinute = schedule.startMinute, - endHour = schedule.endHour, - endMinute = schedule.endMinute - } - ) - -- Update schedule for specific user - week_schedule_table[i].schedules = new_schedule_table - else -- Add schedule for new user - table.insert( - week_schedule_table, - { - userIndex = userIdx, - schedules = {{ - scheduleIndex = scheduleIdx, - weekDays = weekDayList, - startHour = schedule.startHour, - startMinute = schedule.startMinute, - endHour = schedule.endHour, - endMinute = schedule.endMinute - }} - } - ) - end - - device:emit_event(capabilities.lockSchedules.weekDaySchedules(week_schedule_table, {visibility = {displayed = false}})) -end - -local function delete_week_schedule_from_table(device, userIdx, scheduleIdx) - -- Get latest week day schedule table - local week_schedule_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockSchedules.ID, - capabilities.lockSchedules.weekDaySchedules.NAME, - {} - )) - - -- Find schedule for specific user - local i = 0 - for index, entry in pairs(week_schedule_table) do - if entry.userIndex == userIdx then - i = index - end - end - - -- When there is no userIndex in the table - if i == 0 then - return - end - - -- Re-create schedule table for the user - local new_schedule_table = {} - for index, entry in pairs(week_schedule_table[i].schedules) do - if entry.scheduleIndex ~= scheduleIdx then - table.insert(new_schedule_table, entry) - end - end - - -- If user has no schedule, remove user from the table - if #new_schedule_table == 0 then - table.remove(week_schedule_table, i) - else - week_schedule_table[i].schedules = new_schedule_table - end - - device:emit_event(capabilities.lockSchedules.weekDaySchedules(week_schedule_table, {visibility = {displayed = false}})) -end - -local function delete_week_schedule_from_table_as_user(device, userIdx) - -- If User Index is ALL_INDEX, remove all entry from the table - if userIdx == ALL_INDEX then - device:emit_event(capabilities.lockSchedules.weekDaySchedules({}, {visibility = {displayed = false}})) - return - end - - -- Get latest week day schedule table - local week_schedule_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockSchedules.ID, - capabilities.lockSchedules.weekDaySchedules.NAME, - {} - )) - - -- Re-create week day schedule table - local new_week_schedule_table = {} - for index, entry in pairs(week_schedule_table) do - if entry.userIndex ~= userIdx then - table.insert(new_week_schedule_table, entry) - end - end - - device:emit_event(capabilities.lockSchedules.weekDaySchedules(new_week_schedule_table, {visibility = {displayed = false}})) -end - ------------------------------ --- Year Day Schedule Table -- ------------------------------ -local function add_year_schedule_to_table(device, userIdx, scheduleIdx, sTime, eTime) - -- Get latest year day schedule table - local year_schedule_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockSchedules.ID, - capabilities.lockSchedules.yearDaySchedules.NAME, - {} - )) - - -- Find schedule for specific user - local i = 0 - for index, entry in pairs(year_schedule_table) do - if entry.userIndex == userIdx then - i = index - end - end - - if i ~= 0 then -- Add schedule for existing user - -- Exclude same scheduleIdx - local new_schedule_table = {} - for index, entry in pairs(year_schedule_table[i].schedules) do - if entry.scheduleIndex ~= scheduleIdx then - table.insert(new_schedule_table, entry) - end - end - -- Add new entry to table - table.insert( - new_schedule_table, - { - scheduleIndex = scheduleIdx, - localStartTime = sTime, - localEndTime = eTime - } - ) - -- Update schedule for specific user - year_schedule_table[i].schedules = new_schedule_table - else -- Add schedule for new user - table.insert( - year_schedule_table, - { - userIndex = userIdx, - schedules = {{ - scheduleIndex = scheduleIdx, - localStartTime = sTime, - localEndTime = eTime - }} - } - ) - end - - device:emit_event(capabilities.lockSchedules.yearDaySchedules(year_schedule_table, {visibility = {displayed = false}})) -end - -local function delete_year_schedule_from_table(device, userIdx, scheduleIdx) - -- Get latest year day schedule table - local year_schedule_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockSchedules.ID, - capabilities.lockSchedules.yearDaySchedules.NAME, - {} - )) - - -- Find schedule for specific user - local i = 0 - for index, entry in pairs(year_schedule_table) do - if entry.userIndex == userIdx then - i = index - end - end - - -- When there is no userIndex in the table - if i == 0 then - return - end - - -- Re-create year day schedule table for the user - local new_schedule_table = {} - for index, entry in pairs(year_schedule_table[i].schedules) do - if entry.scheduleIndex ~= scheduleIdx then - table.insert(new_schedule_table, entry) - end - end - - -- If user has no schedule, remove user from the table - if #new_schedule_table == 0 then - table.remove(year_schedule_table, i) - else - year_schedule_table[i].schedules = new_schedule_table - end - - device:emit_event(capabilities.lockSchedules.yearDaySchedules(year_schedule_table, {visibility = {displayed = false}})) -end - -local function delete_year_schedule_from_table_as_user(device, userIdx) - -- If User Index is ALL_INDEX, remove all entry from the table - if userIdx == ALL_INDEX then - device:emit_event(capabilities.lockSchedules.yearDaySchedules({}, {visibility = {displayed = false}})) - return - end - - -- Get latest year day schedule table - local year_schedule_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockSchedules.ID, - capabilities.lockSchedules.yearDaySchedules.NAME, - {} - )) - - -- Re-create year day schedule table - local new_year_schedule_table = {} - for index, entry in pairs(year_schedule_table) do - if entry.userIndex ~= userIdx then - table.insert(new_year_schedule_table, entry) - end - end - - device:emit_event(capabilities.lockSchedules.yearDaySchedules(new_year_schedule_table, {visibility = {displayed = false}})) -end - ----------------------------- --- Aliro Credential Table -- ----------------------------- -local function add_aliro_to_table(device, userIdx, keyIdx, keyType, keyId) - -- Get latest aliro table - local aliro_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockAliro.ID, - capabilities.lockAliro.credentials.NAME, - {} - )) - - -- Add new entry to table - table.insert(aliro_table, {userIndex = userIdx, keyIndex = keyIdx, keyType = keyType, keyId = keyId}) - device:emit_event(capabilities.lockAliro.credentials(aliro_table, {visibility = {displayed = false}})) -end - -local function delete_aliro_from_table(device, userIdx, keyType, keyId) - -- Get latest aliro table - local aliro_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockAliro.ID, - capabilities.lockAliro.credentials.NAME, - {} - )) - - -- Delete an entry from aliro table - if keyType == "issuerKey" then - for i, entry in pairs(aliro_table) do - if entry.userIndex == userIdx then - table.remove(aliro_table, i) - break - end - end - else - for i, entry in pairs(aliro_table) do - if entry.userIndex == userIdx and entry.keyId == keyId then - table.remove(aliro_table, i) - break - end - end - end - device:emit_event(capabilities.lockAliro.credentials(aliro_table, {visibility = {displayed = false}})) -end - -local function delete_aliro_from_table_as_user(device, userIdx) - -- If User Index is ALL_INDEX, remove all entry from the table - if userIdx == ALL_INDEX then - device:emit_event(capabilities.lockAliro.credentials({}, {visibility = {displayed = false}})) - return - end - - -- Get latest credential table - local aliro_table = device:get_latest_state( - "main", - capabilities.lockAliro.ID, - capabilities.lockAliro.credentials.NAME - ) or {} - local new_aliro_table = {} - - -- Re-create credential table - for index, entry in pairs(aliro_table) do - if entry.userIndex ~= userIdx then - table.insert(new_aliro_table, entry) - end - end - - device:emit_event(capabilities.lockAliro.credentials(new_aliro_table, {visibility = {displayed = false}})) -end - --------------- --- Add User -- --------------- -local function handle_add_user(driver, device, command) - -- Get parameters - local cmdName = "addUser" - local userName = command.args.userName - local userType = command.args.userType - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) - device:set_field(lock_utils.USER_NAME, userName, {persist = true}) - device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) - - -- Get available user index - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.GetUser(device, ep, INITIAL_CREDENTIAL_INDEX)) -end - ------------------ --- Update User -- ------------------ -local function handle_update_user(driver, device, command) - -- Get parameters - local cmdName = "updateUser" - local userIdx = command.args.userIndex - local userName = command.args.userName - local userNameMatter = string.sub(userName, 1, MAX_USER_NAME_LENGTH) - local userType = command.args.userType - local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER - if userType == "guest" then - userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER - end - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.USER_NAME, userName, {persist = true}) - device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetUser( - device, ep, - DoorLock.types.DataOperationTypeEnum.MODIFY, - userIdx, - userNameMatter, - nil, -- Unique ID - nil, -- User Status - userTypeMatter, - nil -- Credential Rule - ) - ) -end - ------------------------ --- Get User Response -- ------------------------ -local function get_user_response_handler(driver, device, ib, response) - local elements = ib.info_block.data.elements - local userIdx = elements.user_index.value - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local status = "success" - if ib.status == DoorLock.types.DlStatus.FAILURE then - status = "failure" - elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" - end - if status ~= "success" then - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - statusCode = status - } - device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - end - - local ep = find_default_endpoint(device, DoorLock.ID) - local status = elements.user_status.value - local maxUser = device:get_latest_state( - "main", - capabilities.lockUsers.ID, - capabilities.lockUsers.totalUsersSupported.NAME - ) or 10 - - -- Found available user index - if status == nil or status == DoorLock.types.UserStatusEnum.AVAILABLE then - local userName = device:get_field(lock_utils.USER_NAME) - local userNameMatter = string.sub(userName, 1, MAX_USER_NAME_LENGTH) - local userType = device:get_field(lock_utils.USER_TYPE) - local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER - if userType == "guest" then - userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER - end - - -- Save values to field - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - - -- Send command - device:send( - DoorLock.server.commands.SetUser( - device, ep, - DoorLock.types.DataOperationTypeEnum.ADD, - userIdx, - userNameMatter, - nil, -- Unique ID - nil, -- User Status - userTypeMatter, - nil -- Credential Rule - ) - ) - elseif userIdx >= maxUser then -- There's no available user index - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - statusCode = "resourceExhausted" - } - device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - else -- Check next user index - device:send(DoorLock.server.commands.GetUser(device, ep, userIdx + 1)) - end -end - ------------------------ --- Set User Response -- ------------------------ -local function set_user_response_handler(driver, device, ib, response) - -- Get result - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local userIdx = device:get_field(lock_utils.USER_INDEX) - local userName = device:get_field(lock_utils.USER_NAME) - local userType = device:get_field(lock_utils.USER_TYPE) - local status = "success" - if ib.status == DoorLock.types.DlStatus.FAILURE then - status = "failure" - elseif ib.status == DoorLock.types.DlStatus.OCCUPIED then - status = "occupied" - elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" - end - - -- Update User in table - if status == "success" then - if cmdName == "addUser" then - add_user_to_table(device, userIdx, userName, userType) - elseif cmdName == "updateUser" then - update_user_in_table(device, userIdx, userName, userType) - end - else - device.log.warn(string.format("Failed to set user: %s", status)) - end - - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - statusCode = status - } - device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) -end - ------------------ --- Delete User -- ------------------ -local function handle_delete_user(driver, device, command) - -- Get parameters - local cmdName = "deleteUser" - local userIdx = command.args.userIndex - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearUser(device, ep, userIdx)) -end - ----------------------- --- Delete All Users -- ----------------------- -local function handle_delete_all_users(driver, device, command) - -- Get parameters - local cmdName = "deleteAllUsers" - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, ALL_INDEX, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearUser(device, ep, ALL_INDEX)) -end - -------------------------- --- Clear User Response -- -------------------------- -local function clear_user_response_handler(driver, device, ib, response) - -- Get result - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local userIdx = device:get_field(lock_utils.USER_INDEX) - local status = "success" - if ib.status == DoorLock.types.DlStatus.FAILURE then - status = "failure" - elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" - end - - -- Delete User and Credential from table - if status == "success" then - delete_user_from_table(device, userIdx) - delete_credential_from_table_as_user(device, userIdx) - delete_aliro_from_table_as_user(device, userIdx) - delete_week_schedule_from_table_as_user(device, userIdx) - delete_year_schedule_from_table_as_user(device, userIdx) - else - device.log.warn(string.format("Failed to clear user: %s", status)) - end - - -- In the "defaultSchedule" cmd failure path, when a guest user's credentials are set but the scheduling - -- fails during default setup, those credentials should be removed, so we wait to log lock credentials until here. - if cmdName == "defaultSchedule" then - -- note: if clear user succeeds, we'd log credential settings as a "failure" since it's effectively a no-op. - -- If clear user fails, log "success" since the credentials would still be present. - local lock_credential_status = status == "success" and "failure" or "success" - local command_result_info = { - commandName = "addCredential", - userIndex = userIdx, - statusCode = lock_credential_status - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - else - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - statusCode = status - } - device:emit_event(capabilities.lockUsers.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - end - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) -end - --------------------- --- Add Credential -- --------------------- -local function handle_add_credential(driver, device, command) - -- Get parameters - local cmdName = "addCredential" - local userIdx = command.args.userIndex - local userType = command.args.userType - local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER - if userType == "guest" then - userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER - end - if userIdx == 0 then - userIdx = nil - else - userTypeMatter = nil - end - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = INITIAL_CREDENTIAL_INDEX - } - local credData = command.args.credentialData - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.USER_TYPE, userType, {persist = true}) - device:set_field(lock_utils.CRED_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) - device:set_field(lock_utils.CRED_DATA, credData, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetCredential( - device, ep, - DoorLock.types.DataOperationTypeEnum.ADD, -- Data Operation Type: Add(0), Modify(2) - credential, -- Credential - credData, -- Credential Data - userIdx, -- User Index - nil, -- User Status - userTypeMatter -- User Type - ) - ) -end - ------------------------ --- Update Credential -- ------------------------ -local function handle_update_credential(driver, device, command) - -- Get parameters - local cmdName = "updateCredential" - local userIdx = command.args.userIndex - local credIdx = command.args.credentialIndex - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = credIdx - } - local credData = command.args.credentialData - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) - device:set_field(lock_utils.USER_TYPE, nil, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetCredential( - device, ep, - DoorLock.types.DataOperationTypeEnum.MODIFY, -- Data Operation Type: Add(0), Modify(2) - credential, -- Credential - credData, -- Credential Data - userIdx, -- User Index - nil, -- User Status - nil -- User Type - ) - ) -end - ---------------------------------- --- Set Pin Credential Response -- ---------------------------------- -local function set_pin_response_handler(driver, device, ib, response) - if ib.status ~= im.InteractionResponse.Status.SUCCESS then - device.log.error("Failed to set credential for device") - return - end - - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local credData = device:get_field(lock_utils.CRED_DATA) - if cmdName == "addCota" then - credData = device:get_field(lock_utils.COTA_CRED) - end - local userIdx = device:get_field(lock_utils.USER_INDEX) - local userType = device:get_field(lock_utils.USER_TYPE) - local credIdx = device:get_field(lock_utils.CRED_INDEX) - local elements = ib.info_block.data.elements - local status = RESPONSE_STATUS_MAP[elements.status.value] - - if status == "success" then - -- Don't save user and credential for COTA - if cmdName == "addCota" then - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - return - end - - -- If user is added also, update User table - if userIdx == nil then - add_user_to_table(device, elements.user_index.value, nil, userType) - end - - -- Update Credential table - userIdx = elements.user_index.value - if cmdName == "addCredential" then - add_credential_to_table(device, userIdx, credIdx, "pin") - end - - -- If User Type is Guest and device support schedule, add default schedule - local week_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.WEEK_DAY_ACCESS_SCHEDULES}) - local year_schedule_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.YEAR_DAY_ACCESS_SCHEDULES}) - if userType == "guest" and (#week_schedule_eps > 0 or #year_schedule_eps > 0) and cmdName ~= "updateCredential" then - local cmdName = "defaultSchedule" - local scheduleIdx = 1 - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) - - local ep = device:component_to_endpoint("main") - device:send( - DoorLock.server.commands.SetYearDaySchedule( - device, ep, - scheduleIdx, - userIdx, - MIN_EPOCH_S, - MAX_EPOCH_S - ) - ) - else - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - credentialIndex = credIdx, - statusCode = status - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - end - return - end - - -- In the case DlStatus returns Occupied, this means the current credential index is in use, - -- so we must try the next one. If there is not a next index (i.e. it is nil), - -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. - device.log.warn(string.format("Failed to set credential: %s", status)) - if status == "occupied" and elements.next_credential_index.value == nil then - local command_result_info = { - commandName = cmdName, - statusCode = "resourceExhausted" -- No more available credential index - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - elseif status == "occupied" then - -- Get parameters - local credIdx = elements.next_credential_index.value - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = credIdx, - } - local userIdx = device:get_field(lock_utils.USER_INDEX) - local userType = device:get_field(lock_utils.USER_TYPE) - local userTypeMatter = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER - if userIdx ~= nil then - userTypeMatter = nil - elseif userType == "guest" then - userTypeMatter = DoorLock.types.UserTypeEnum.SCHEDULE_RESTRICTED_USER - elseif userType == "remote" then - userTypeMatter = DoorLock.types.UserTypeEnum.REMOTE_ONLY_USER - end - - device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) - - -- Send command - local ep = find_default_endpoint(device, DoorLock.ID) - device:send( - DoorLock.server.commands.SetCredential( - device, ep, - DoorLock.types.DataOperationTypeEnum.ADD, -- Data Operation Type: Add(0), Modify(2) - credential, -- Credential - credData, -- Credential Data - userIdx, -- User Index - nil, -- User Status - userTypeMatter -- User Type - ) - ) - elseif status == "duplicate" and cmdName == "addCota" then - generate_cota_cred_for_device(device) - device.thread:call_with_delay(0, function(t) set_cota_credential(device, credIdx) end) - else - local command_result_info = { - commandName = cmdName, - statusCode = status - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - end -end - ------------------------------------ --- Set Aliro Credential Response -- ------------------------------------ -local function set_issuer_key_response_handler(driver, device, ib, response) - local cmdName = "setIssuerKey" - local userIdx = device:get_field(lock_utils.USER_INDEX) - local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER - local issuerKeyIndex = device:get_field(lock_utils.ISSUER_KEY_INDEX) - local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) - local elements = ib.info_block.data.elements - local status = RESPONSE_STATUS_MAP[elements.status.value] - - if status == "success" then - -- Delete field data - device:set_field(lock_utils.ISSUER_KEY, nil, {persist = true}) - - -- If user is added also, update User table - if userIdx == nil then - userIdx = elements.user_index.value - add_user_to_table(device, userIdx, nil, "adminMember") - end - - -- Update Aliro table - add_aliro_to_table(device, userIdx, issuerKeyIndex, "issuerKey", nil) - - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - requestId = reqId, - statusCode = status - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - return - end - - -- In the case DlStatus returns Occupied, this means the current credential index is in use, - -- so we must try the next one. If there is not a next index (i.e. it is nil), - -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. - device.log.warn(string.format("Failed to set credential: %s", status)) - if status == "occupied" and elements.next_credential_index.value == nil then - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - requestId = reqId, - statusCode = "resourceExhausted" -- No more available credential index - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - elseif status == "occupied" then - -- Get parameters - if userIdx ~= nil then - userType = nil - end - local credIdx = elements.next_credential_index.value - local credType = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY - local credData = device:get_field(lock_utils.ISSUER_KEY) - local credential = { - credential_type = credType, - credential_index = credIdx - } - - -- Save values to field - device:set_field(lock_utils.ISSUER_KEY_INDEX, credIdx, {persist = true}) - - -- Send command - local ep = find_default_endpoint(device, DoorLock.ID) - device:send( - DoorLock.server.commands.SetCredential( - device, ep, - DoorLock.types.DataOperationTypeEnum.ADD, - credential, -- Credential - lock_utils.hex_string_to_octet_string(credData), -- Credential Data - userIdx, -- User Index - nil, -- User Status - userType -- User Type - ) - ) - else - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - requestId = reqId, - statusCode = status - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - end -end - -local function set_endpoint_key_response_handler(driver, device, ib, response) - local cmdName = "setEndpointKey" - local userIdx = device:get_field(lock_utils.USER_INDEX) - local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER - local keyId = device:get_field(lock_utils.DEVICE_KEY_ID) - local keyType = device:get_field(lock_utils.ENDPOINT_KEY_TYPE) - local endpointKeyIndex = device:get_field(lock_utils.ENDPOINT_KEY_INDEX) - local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) - local elements = ib.info_block.data.elements - local status = RESPONSE_STATUS_MAP[elements.status.value] - - if status == "success" then - -- Delete field data - device:set_field(lock_utils.ENDPOINT_KEY, nil, {persist = true}) - - -- If user is added also, update User table - if userIdx == nil then - userIdx = elements.user_index.value - add_user_to_table(device, userIdx, nil, "adminMember") - end - - -- Update Aliro table - add_aliro_to_table(device, userIdx, endpointKeyIndex, keyType, keyId) - - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - keyId = keyId, - requestId = reqId, - statusCode = status - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - return - end - - -- In the case DlStatus returns Occupied, this means the current credential index is in use, - -- so we must try the next one. If there is not a next index (i.e. it is nil), - -- we should mark this as "resourceExhausted" and stop attempting to set the credentials. - device.log.warn(string.format("Failed to set credential: %s", status)) - - if status == "occupied" and elements.next_credential_index.value == nil then - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - keyId = keyId, - requestId = reqId, - statusCode = "resourceExhausted" -- No more available credential index - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - elseif status == "occupied" then - -- Get parameters - if userIdx ~= nil then - userType = nil - end - local credIdx = elements.next_credential_index.value - local credType = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType] - local credData = device:get_field(lock_utils.ENDPOINT_KEY) - local credential = { - credential_type = credType, - credential_index = credIdx - } - - -- Save values to field - device:set_field(lock_utils.ENDPOINT_KEY_INDEX, credIdx, {persist = true}) - - -- Send command - local ep = find_default_endpoint(device, DoorLock.ID) - device:send( - DoorLock.server.commands.SetCredential( - device, ep, - DoorLock.types.DataOperationTypeEnum.ADD, - credential, -- Credential - lock_utils.hex_string_to_octet_string(credData), -- Credential Data - userIdx, -- User Index - nil, -- User Status - userType -- User Type - ) - ) - else - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - keyId = keyId, - requestId = reqId, - statusCode = status - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - end -end - -local function set_credential_response_handler(driver, device, ib, response) - if ib.status ~= im.InteractionResponse.Status.SUCCESS then - device.log.error("Failed to set credential for device") - return - end - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - if cmdName == "addCredential" or cmdName == "updateCredential" or cmdName == "addCota" then - set_pin_response_handler(driver, device, ib, response) - elseif cmdName == "setIssuerKey" then - set_issuer_key_response_handler(driver, device, ib, response) - elseif cmdName == "setEndpointKey" then - set_endpoint_key_response_handler(driver, device, ib, response) - end -end - ------------------------ --- Delete Credential -- ------------------------ -local function handle_delete_credential(driver, device, command) - -- Get parameters - local cmdName = "deleteCredential" - local credIdx = command.args.credentialIndex - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = credIdx, - } - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.CRED_INDEX, credIdx, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) -end - ----------------------------- --- Delete All Credentials -- ----------------------------- -local function handle_delete_all_credentials(driver, device, command) - -- Get parameters - local cmdName = "deleteAllCredentials" - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.PIN, - credential_index = ALL_INDEX, - } - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.CRED_INDEX, ALL_INDEX, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) -end - -------------------------------- --- Clear Credential Response -- -------------------------------- -local function clear_credential_response_handler(driver, device, ib, response) - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - if cmdName ~= "deleteCredential" and cmdName ~= "clearEndpointKey" and - cmdName ~= "clearIssuerKey" and cmdName ~= "deleteAllCredentials" then - return - end - local status = RESPONSE_STATUS_MAP[ib.status] or "success" - local command_result_info = { commandName = cmdName, statusCode = status } -- default command result - local userIdx = device:get_field(lock_utils.USER_INDEX) - local all_user_credentials_removed = false - - if (cmdName == "deleteCredential" or cmdName == "deleteAllCredentials") and status == "success" then - -- Get result from data saved in relevant, associated fields - local credIdx = device:get_field(lock_utils.CRED_INDEX) - - -- find userIdx associated with credIdx, don't use lock utils field in this case - userIdx = delete_credential_from_table(device, credIdx) - if userIdx ~= nil then - all_user_credentials_removed = not has_credentials(device, userIdx) - end - - -- set unique command result fields - command_result_info.userIndex = userIdx - command_result_info.credentialIndex = credIdx - elseif cmdName == "clearIssuerKey" and status == "success" then - -- Get result from data saved in relevant, associated fields - local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) - - delete_aliro_from_table(device, userIdx, "issuerKey", nil) - all_user_credentials_removed = not has_credentials(device, userIdx) - - -- set unique command result fields - command_result_info.userIndex = userIdx - command_result_info.requestId = reqId - elseif cmdName == "clearEndpointKey" and status == "success" then - -- Get result from data saved in relevant, associated fields - local deviceKeyId = device:get_field(lock_utils.DEVICE_KEY_ID) - local keyType = device:get_field(lock_utils.ENDPOINT_KEY_TYPE) - local reqId = device:get_field(lock_utils.COMMAND_REQUEST_ID) - - delete_aliro_from_table(device, userIdx, keyType, deviceKeyId) - all_user_credentials_removed = not has_credentials(device, userIdx) - - -- set unique command result fields - command_result_info.userIndex = userIdx - command_result_info.keyId = deviceKeyId - command_result_info.requestId = reqId - end - - -- user data if credentials were removed - if all_user_credentials_removed then - delete_user_from_table(device, userIdx) - delete_week_schedule_from_table_as_user(device, userIdx) - delete_year_schedule_from_table_as_user(device, userIdx) - end - - -- Update commandResult - if cmdName == "deleteCredential" or cmdName == "deleteAllCredentials" then - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - else - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - end - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) -end - ---------------------------- --- Set Week Day Schedule -- ---------------------------- -local function handle_set_week_day_schedule(driver, device, command) - -- Get parameters - local cmdName = "setWeekDaySchedule" - local scheduleIdx = command.args.scheduleIndex - local userIdx = command.args.userIndex - local schedule = command.args.schedule - local wDays = {} - local scheduleBit = 0 - for _, weekDay in ipairs(schedule.weekDays) do - scheduleBit = scheduleBit + WEEK_DAY_MAP[weekDay] - table.insert(wDays, weekDay) - end - local startHour = schedule.startHour - local startMinute = schedule.startMinute - local endHour = schedule.endHour - local endMinute = schedule.endMinute - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockSchedules.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) - device:set_field(lock_utils.SCHEDULE_WEEK_DAYS, wDays, {persist = true}) - device:set_field(lock_utils.SCHEDULE_START_HOUR, startHour, {persist = true}) - device:set_field(lock_utils.SCHEDULE_START_MINUTE, startMinute, {persist = true}) - device:set_field(lock_utils.SCHEDULE_END_HOUR, endHour, {persist = true}) - device:set_field(lock_utils.SCHEDULE_END_MINUTE, endMinute, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetWeekDaySchedule( - device, ep, - scheduleIdx, -- Week Day Schedule Index - userIdx, -- User Index - scheduleBit, -- Days Mask - startHour, -- Start Hour - startMinute, -- Start Minute - endHour, -- End Hour - endMinute -- End Minute - ) - ) -end - ------------------------------------- --- Set Week Day Schedule Response -- ------------------------------------- -local function set_week_day_schedule_handler(driver, device, ib, response) - -- Get result - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local userIdx = device:get_field(lock_utils.USER_INDEX) - local scheduleIdx = device:get_field(lock_utils.SCHEDULE_INDEX) - local days = device:get_field(lock_utils.SCHEDULE_WEEK_DAYS) - local sHour = device:get_field(lock_utils.SCHEDULE_START_HOUR) - local sMinute = device:get_field(lock_utils.SCHEDULE_START_MINUTE) - local eHour = device:get_field(lock_utils.SCHEDULE_END_HOUR) - local eMinute = device:get_field(lock_utils.SCHEDULE_END_MINUTE) - local schedule = { - weekDays = days, - startHour = sHour, - startMinute = sMinute, - endHour = eHour, - endMinute = eMinute - } - local status = "success" - if ib.status == DoorLock.types.DlStatus.FAILURE then - status = "failure" - elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" - end - - -- Add Week Day Schedule to table - if status == "success" then - add_week_schedule_to_table(device, userIdx, scheduleIdx, schedule) - else - device.log.warn(string.format("Failed to set week day schedule: %s", status)) - end - - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - scheduleIndex = scheduleIdx, - statusCode = status - } - device:emit_event(capabilities.lockSchedules.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) -end - ------------------------------ --- Clear Week Day Schedule -- ------------------------------ -local function handle_clear_week_day_schedule(driver, device, command) - -- Get parameters - local cmdName = "clearWeekDaySchedules" - local scheduleIdx = command.args.scheduleIndex - local userIdx = command.args.userIndex - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockSchedules.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearWeekDaySchedule(device, ep, scheduleIdx, userIdx)) -end - ------------------------------------- --- Clear Week Day Schedule Response -- ------------------------------------- -local function clear_week_day_schedule_handler(driver, device, ib, response) - -- Get result - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local scheduleIdx = device:get_field(lock_utils.SCHEDULE_INDEX) - local userIdx = device:get_field(lock_utils.USER_INDEX) - local status = "success" - if ib.status == DoorLock.types.DlStatus.FAILURE then - status = "failure" - elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" - end - - -- Delete Week Day Schedule to table - if status == "success" then - delete_week_schedule_from_table(device, userIdx, scheduleIdx) - else - device.log.warn(string.format("Failed to clear week day schedule: %s", status)) - end - - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - scheduleIndex = scheduleIdx, - statusCode = status - } - device:emit_event(capabilities.lockSchedules.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) -end - --- This type represents an offset, in seconds, from 0 hours, 0 minutes, 0 seconds, on the 1st of January, 2000 UTC -local function iso8601_to_epoch(iso_str) - local pattern = "^(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)" - local year, month, day, hour, min, sec = iso_str:match(pattern) - if not year then - return nil - end - local epoch_s = os.time({ - year = tonumber(year), - month = tonumber(month), - day = tonumber(day), - hour = tonumber(hour), - min = tonumber(min), - sec = tonumber(sec), - }) - - -- The os.time() is based on 1970. Thirty years must be subtracted for calculations from 2000. - epoch_s = epoch_s - THIRTY_YEARS_S - - if epoch_s < MIN_EPOCH_S then - return MIN_EPOCH_S - elseif epoch_s > MAX_EPOCH_S then - return MAX_EPOCH_S - else - return epoch_s - end -end - ---------------------------- --- Set Year Day Schedule -- ---------------------------- -local function handle_set_year_day_schedule(driver, device, command) - -- Get parameters - local cmdName = "setYearDaySchedule" - local scheduleIdx = command.args.scheduleIndex - local userIdx = command.args.userIndex - local localStartTime = command.args.schedule.localStartTime - local localEndTime = command.args.schedule.localEndTime - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockSchedules.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) - device:set_field(lock_utils.SCHEDULE_LOCAL_START_TIME, localStartTime, {persist = true}) - device:set_field(lock_utils.SCHEDULE_LOCAL_END_TIME, localEndTime, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetYearDaySchedule( - device, ep, - scheduleIdx, - userIdx, - iso8601_to_epoch(localStartTime), - iso8601_to_epoch(localEndTime) - ) - ) -end - ------------------------------------- --- Set Year Day Schedule Response -- ------------------------------------- -local function set_year_day_schedule_handler(driver, device, ib, response) - -- Get result - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local userIdx = device:get_field(lock_utils.USER_INDEX) - local scheduleIdx = device:get_field(lock_utils.SCHEDULE_INDEX) - local localStartTime = device:get_field(lock_utils.SCHEDULE_LOCAL_START_TIME) - local localEndTime = device:get_field(lock_utils.SCHEDULE_LOCAL_END_TIME) - local status = "success" - if ib.status == DoorLock.types.DlStatus.FAILURE then - status = "failure" - elseif ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" - end - - if cmdName == "defaultSchedule" then - local cmdName = "addCredential" - local credIdx = device:get_field(lock_utils.CRED_INDEX) - - if status == "success" then - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - credentialIndex = credIdx, - statusCode = status - } - device:emit_event(capabilities.lockCredentials.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - else - local ep = find_default_endpoint(device, clusters.DoorLock.ID) - device:send(DoorLock.server.commands.ClearUser(device, ep, userIdx)) - end - return - end - - if status == "success" then - if cmdName == "setYearDaySchedule" then - add_year_schedule_to_table(device, userIdx, scheduleIdx, localStartTime, localEndTime) - elseif cmdName == "clearYearDaySchedules" then - delete_year_schedule_from_table(device, userIdx, scheduleIdx) - end - else - device.log.warn(string.format("Failed to set/clear year day schedule: %s", status)) - end - - -- Update commandResult - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - scheduleIndex = scheduleIdx, - statusCode = status - } - device:emit_event(capabilities.lockSchedules.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) -end - ------------------------------ --- Clear Year Day Schedule -- ------------------------------ -local function handle_clear_year_day_schedule(driver, device, command) - -- Get parameters - local cmdName = "clearYearDaySchedules" - local scheduleIdx = command.args.scheduleIndex - local userIdx = command.args.userIndex - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockSchedules.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.SCHEDULE_INDEX, scheduleIdx, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - - -- Send command - -- In SmartThings, Schedule Restrict User is basically allowed to access always. - -- So, if user delete the year day schedule, enter an infinitely long schedule. - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetYearDaySchedule( - device, ep, - scheduleIdx, - userIdx, - MIN_EPOCH_S, - MAX_EPOCH_S - ) - ) -end - ----------------- --- Lock Alarm -- ----------------- -local function alarm_event_handler(driver, device, ib, response) - local DlAlarmCode = DoorLock.types.DlAlarmCode - local alarm_code = ib.data.elements.alarm_code - if alarm_code.value == DlAlarmCode.LOCK_JAMMED then - device:emit_event(capabilities.lockAlarm.alarm.unableToLockTheDoor({state_change = true})) - elseif alarm_code.value == DlAlarmCode.LOCK_FACTORY_RESET then - device:emit_event(capabilities.lockAlarm.alarm.lockFactoryReset({state_change = true})) - elseif alarm_code.value == DlAlarmCode.WRONG_CODE_ENTRY_LIMIT then - device:emit_event(capabilities.lockAlarm.alarm.attemptsExceeded({state_change = true})) - elseif alarm_code.value == DlAlarmCode.FRONT_ESCEUTCHEON_REMOVED then - device:emit_event(capabilities.lockAlarm.alarm.damaged({state_change = true})) - elseif alarm_code.value == DlAlarmCode.DOOR_FORCED_OPEN then - device:emit_event(capabilities.lockAlarm.alarm.forcedOpeningAttempt({state_change = true})) - end -end - --------------------- --- Lock Operation -- --------------------- -local function lock_op_event_handler(driver, device, ib, response) - local opType = ib.data.elements.lock_operation_type - local opSource = ib.data.elements.operation_source - local userIdx = ib.data.elements.user_index - -- TODO: This handler can check fabric index and exclude other fabric events - - if opType == nil or opSource == nil then - return - end - - local Type = DoorLock.types.LockOperationTypeEnum - local Lock = capabilities.lock.lock - if opType.value == Type.LOCK then - opType = Lock.locked - elseif opType.value == Type.UNLOCK then - opType = Lock.unlocked - elseif opType.value == Type.UNLATCH then - opType = Lock.unlatched - else - return - end - - local Source = DoorLock.types.OperationSourceEnum - if opSource.value == Source.UNSPECIFIED then - opSource = nil - elseif opSource.value == Source.MANUAL then - opSource = "manual" - elseif opSource.value == Source.PROPRIETARY_REMOTE then - opSource = "proprietaryRemote" - elseif opSource.value == Source.KEYPAD then - opSource = "keypad" - elseif opSource.value == Source.AUTO then - opSource = "auto" - elseif opSource.value == Source.BUTTON then - opSource = "button" - elseif opSource.value == Source.SCHEDULE then - opSource = nil - elseif opSource.value == Source.REMOTE then - opSource = "command" - elseif opSource.value == Source.RFID then - opSource = "rfid" - elseif opSource.value == Source.BIOMETRIC then - opSource = nil -- It will be updated R2 - elseif opSource.value == Source.ALIRO then - opSource = "digitalKey" - else - opSource = nil - end - - if userIdx ~= nil then - userIdx = userIdx.value - end - - local data_obj = {method = opSource, userIndex = userIdx} - device:emit_event(opType({data = data_obj, state_change = true})) -end - -local function handle_refresh(driver, device, command) - local req = DoorLock.attributes.LockState:read(device) - device:send(req) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) -end - -local function handle_set_reader_config(driver, device, command) - local cmdName = "setReaderConfig" - local signingKey = command.args.signingKey - local verificationKey = command.args.verificationKey - local groupId = command.args.groupId - local groupResolvingKey = nil - local aliro_ble_uwb_eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.Feature.ALIROBLEUWB}) - if #aliro_ble_uwb_eps > 0 then - groupResolvingKey = command.args.groupResolvingKey - end - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - statusCode = "busy" - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - local reader_config_updated = device:get_field(lock_utils.ALIRO_READER_CONFIG_UPDATED) or nil - if reader_config_updated == "IN_PROGRESS" then - return - elseif reader_config_updated == "TRUE" then - -- Update commandResult - local command_result_info = { - commandName = cmdName, - statusCode = "success" - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.VERIFICATION_KEY, verificationKey, {persist = true}) - device:set_field(lock_utils.GROUP_ID, groupId, {persist = true}) - device:set_field(lock_utils.GROUP_RESOLVING_KEY, groupResolvingKey, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetAliroReaderConfig( - device, ep, - lock_utils.hex_string_to_octet_string(signingKey), - lock_utils.hex_string_to_octet_string(verificationKey), - lock_utils.hex_string_to_octet_string(groupId), - lock_utils.hex_string_to_octet_string(groupResolvingKey) - ) - ) - device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "IN_PROGRESS", {persist = true}) -end - -local function set_aliro_reader_config_handler(driver, device, ib, response) - -- Get result - local cmdName = device:get_field(lock_utils.COMMAND_NAME) - local status = "failure" - if ib.status == DoorLock.types.DlStatus.SUCCESS then - status = "success" - device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, "TRUE", {persist = true}) - else - if ib.status == DoorLock.types.DlStatus.INVALID_FIELD then - status = "invalidCommand" - end - device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, nil, {persist = true}) - end - - -- Update commandResult - local command_result_info = { - commandName = cmdName, - statusCode = status - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) -end - -local function handle_set_card_id(driver, device, command) - if command.args.cardId ~= nil then - device:emit_event(capabilities.lockAliro.cardId(command.args.cardId, {visibility = {displayed = false}})) - end -end - -local function handle_set_issuer_key(driver, device, command) - -- Get parameters - local cmdName = "setIssuerKey" - local userIdx = command.args.userIndex - local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER - local issuerKey = command.args.issuerKey - local reqId = command.args.requestId - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, - credential_index = INITIAL_CREDENTIAL_INDEX - } - - -- Adjustment - if userIdx == 0 then - userIdx = nil - else - userType = nil - end - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - requestId = reqId, - statusCode = "busy" - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.ISSUER_KEY, issuerKey, {persist = true}) - device:set_field(lock_utils.ISSUER_KEY_INDEX, INITIAL_CREDENTIAL_INDEX, {persist = true}) - device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetCredential( - device, ep, - DoorLock.types.DataOperationTypeEnum.ADD, -- Data Operation Type: Add(0), Modify(2) - credential, -- Credential - lock_utils.hex_string_to_octet_string(issuerKey), -- Credential Data - userIdx, -- User Index - nil, -- User Status - userType -- User Type - ) - ) -end - -local function handle_clear_issuer_key(driver, device, command) - -- Get parameters - local cmdName = "clearIssuerKey" - local userIdx = command.args.userIndex - local reqId = command.args.requestId - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - requestId = reqId, - statusCode = "busy" - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) - - -- Get latest aliro table - local aliro_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockAliro.ID, - capabilities.lockAliro.credentials.NAME, - {} - )) - - -- Find issuer key index - for index, entry in pairs(aliro_table) do - if entry.userIndex == userIdx and entry.keyType == "issuerKey" then - -- Set parameters - local credential = { - credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, - credential_index = entry.keyIndex, - } - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) - break - end - end -end - -local function handle_set_endpoint_key(driver, device, command) - -- Get parameters - local cmdName = "setEndpointKey" - local userIdx = command.args.userIndex - local userType = DoorLock.types.UserTypeEnum.UNRESTRICTED_USER - local keyId = command.args.keyId - local keyType = command.args.keyType - local endpointKey = command.args.endpointKey - local reqId = command.args.requestId - local dataOpType = DoorLock.types.DataOperationTypeEnum.ADD -- Data Operation Type: Add(0), Modify(2) - local endpointKeyIndex = INITIAL_CREDENTIAL_INDEX - - -- Min user index of commandResult is 1 - -- 0 should convert to nil before busy check - if userIdx == 0 then - userIdx = nil - end - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - keyId = keyId, - requestId = reqId, - statusCode = "busy" - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Adjustment - if userIdx ~= nil then - userType = nil - - -- Get latest aliro table - local aliro_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockAliro.ID, - capabilities.lockAliro.credentials.NAME, - {} - )) - - -- Find existing endpoint key - for index, entry in pairs(aliro_table) do - if (entry.keyType == "evictableEndpointKey" or entry.keyType == "nonEvictableEndpointKey") and entry.keyId == keyId then - dataOpType = DoorLock.types.DataOperationTypeEnum.MODIFY - endpointKeyIndex = entry.keyIndex - delete_aliro_from_table(device, userIdx, keyType, keyId) - break - end - end - end - - local credential = { - credential_type = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType], - credential_index = endpointKeyIndex - } - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.DEVICE_KEY_ID, keyId, {persist = true}) - device:set_field(lock_utils.ENDPOINT_KEY_TYPE, keyType, {persist = true}) - device:set_field(lock_utils.ENDPOINT_KEY, endpointKey, {persist = true}) - device:set_field(lock_utils.ENDPOINT_KEY_INDEX, endpointKeyIndex, {persist = true}) - device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) - - -- Send command - local ep = device:component_to_endpoint(command.component) - device:send( - DoorLock.server.commands.SetCredential( - device, ep, - dataOpType, -- Data Operation Type: Add(0), Modify(2) - credential, -- Credential - lock_utils.hex_string_to_octet_string(endpointKey), -- Credential Data - userIdx, -- User Index - nil, -- User Status - userType -- User Type - ) - ) -end - -local function handle_clear_endpoint_key(driver, device, command) - -- Get parameters - local cmdName = "clearEndpointKey" - local userIdx = command.args.userIndex - local keyId = command.args.keyId - local keyType = command.args.keyType - local reqId = command.args.requestId - - -- Check busy state - if lock_utils.is_busy_state_set(device) then - local command_result_info = { - commandName = cmdName, - userIndex = userIdx, - keyId = keyId, - requestId = reqId, - statusCode = "busy" - } - device:emit_event(capabilities.lockAliro.commandResult( - command_result_info, {state_change = true, visibility = {displayed = false}} - )) - return - end - - -- Save values to field - device:set_field(lock_utils.COMMAND_NAME, cmdName, {persist = true}) - device:set_field(lock_utils.USER_INDEX, userIdx, {persist = true}) - device:set_field(lock_utils.DEVICE_KEY_ID, keyId, {persist = true}) - device:set_field(lock_utils.ENDPOINT_KEY_TYPE, keyType, {persist = true}) - device:set_field(lock_utils.COMMAND_REQUEST_ID, reqId, {persist = true}) - - -- Get latest aliro table - local aliro_table = utils.deep_copy(device:get_latest_state( - "main", - capabilities.lockAliro.ID, - capabilities.lockAliro.credentials.NAME, - {} - )) - - local ep = device:component_to_endpoint(command.component) - if keyId == nil then - return - else - -- Find aliro credential - for index, entry in pairs(aliro_table) do - if entry.userIndex == userIdx and entry.keyId == keyId and entry.keyType == keyType then - -- Set parameters - local credential = { - credential_type = ALIRO_KEY_TYPE_TO_CRED_ENUM_MAP[keyType], - credential_index = entry.keyIndex, - } - -- Send command - device:send(DoorLock.server.commands.ClearCredential(device, ep, credential)) - break - end - end - end -end - -local new_matter_lock_handler = { - NAME = "New Matter Lock Handler", - lifecycle_handlers = { - init = device_init, - added = device_added, - doConfigure = do_configure, - infoChanged = info_changed, - driverSwitched = driver_switched - }, - matter_handlers = { - attr = { - [DoorLock.ID] = { - [DoorLock.attributes.LockState.ID] = lock_state_handler, - [DoorLock.attributes.OperatingMode.ID] = operating_modes_handler, - [DoorLock.attributes.NumberOfTotalUsersSupported.ID] = total_users_supported_handler, - [DoorLock.attributes.NumberOfPINUsersSupported.ID] = pin_users_supported_handler, - [DoorLock.attributes.MinPINCodeLength.ID] = min_pin_code_len_handler, - [DoorLock.attributes.MaxPINCodeLength.ID] = max_pin_code_len_handler, - [DoorLock.attributes.RequirePINforRemoteOperation.ID] = require_remote_pin_handler, - [DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser.ID] = max_week_schedule_of_user_handler, - [DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser.ID] = max_year_schedule_of_user_handler, - [DoorLock.attributes.AliroReaderVerificationKey.ID] = aliro_reader_verification_key_handler, - [DoorLock.attributes.AliroReaderGroupIdentifier.ID] = aliro_reader_group_id_handler, - [DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions.ID] = aliro_protocol_versions_handler, - [DoorLock.attributes.AliroGroupResolvingKey.ID] = aliro_group_resolving_key_handler, - [DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions.ID] = aliro_supported_ble_uwb_protocol_versions_handler, - [DoorLock.attributes.AliroBLEAdvertisingVersion.ID] = aliro_ble_advertising_version_handler, - [DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported.ID] = max_aliro_credential_issuer_key_handler, - [DoorLock.attributes.NumberOfAliroEndpointKeysSupported.ID] = max_aliro_endpoint_key_handler, - [DoorLockFeatureMapAttr.ID] = door_lock_feature_map_handler, - }, - [PowerSource.ID] = { - [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, - [PowerSource.attributes.BatPercentRemaining.ID] = handle_battery_percent_remaining, - [PowerSource.attributes.BatChargeLevel.ID] = handle_battery_charge_level, - } - }, - event = { - [DoorLock.ID] = { - [DoorLock.events.DoorLockAlarm.ID] = alarm_event_handler, - [DoorLock.events.LockOperation.ID] = lock_op_event_handler, - }, - }, - cmd_response = { - [DoorLock.ID] = { - [DoorLock.server.commands.SetUser.ID] = set_user_response_handler, - [DoorLock.client.commands.GetUserResponse.ID] = get_user_response_handler, - [DoorLock.server.commands.ClearUser.ID] = clear_user_response_handler, - [DoorLock.client.commands.SetCredentialResponse.ID] = set_credential_response_handler, - [DoorLock.server.commands.ClearCredential.ID] = clear_credential_response_handler, - [DoorLock.server.commands.SetWeekDaySchedule.ID] = set_week_day_schedule_handler, - [DoorLock.server.commands.ClearWeekDaySchedule.ID] = clear_week_day_schedule_handler, - [DoorLock.server.commands.SetYearDaySchedule.ID] = set_year_day_schedule_handler, - [DoorLock.server.commands.SetAliroReaderConfig.ID] = set_aliro_reader_config_handler, - }, - }, - }, - subscribed_attributes = subscribed_attributes, - subscribed_events = subscribed_events, - capability_handlers = { - [capabilities.lock.ID] = { - [capabilities.lock.commands.lock.NAME] = handle_lock, - [capabilities.lock.commands.unlock.NAME] = handle_unlock, - [capabilities.lock.commands.unlatch.NAME] = handle_unlatch - }, - [capabilities.lockUsers.ID] = { - [capabilities.lockUsers.commands.addUser.NAME] = handle_add_user, - [capabilities.lockUsers.commands.updateUser.NAME] = handle_update_user, - [capabilities.lockUsers.commands.deleteUser.NAME] = handle_delete_user, - [capabilities.lockUsers.commands.deleteAllUsers.NAME] = handle_delete_all_users, - }, - [capabilities.lockCredentials.ID] = { - [capabilities.lockCredentials.commands.addCredential.NAME] = handle_add_credential, - [capabilities.lockCredentials.commands.updateCredential.NAME] = handle_update_credential, - [capabilities.lockCredentials.commands.deleteCredential.NAME] = handle_delete_credential, - [capabilities.lockCredentials.commands.deleteAllCredentials.NAME] = handle_delete_all_credentials, - }, - [capabilities.lockSchedules.ID] = { - [capabilities.lockSchedules.commands.setWeekDaySchedule.NAME] = handle_set_week_day_schedule, - [capabilities.lockSchedules.commands.clearWeekDaySchedules.NAME] = handle_clear_week_day_schedule, - [capabilities.lockSchedules.commands.setYearDaySchedule.NAME] = handle_set_year_day_schedule, - [capabilities.lockSchedules.commands.clearYearDaySchedules.NAME] = handle_clear_year_day_schedule, - }, - [capabilities.lockAliro.ID] = { - [capabilities.lockAliro.commands.setReaderConfig.NAME] = handle_set_reader_config, - [capabilities.lockAliro.commands.setCardId.NAME] = handle_set_card_id, - [capabilities.lockAliro.commands.setIssuerKey.NAME] = handle_set_issuer_key, - [capabilities.lockAliro.commands.clearIssuerKey.NAME] = handle_clear_issuer_key, - [capabilities.lockAliro.commands.setEndpointKey.NAME] = handle_set_endpoint_key, - [capabilities.lockAliro.commands.clearEndpointKey.NAME] = handle_clear_endpoint_key, - }, - [capabilities.refresh.ID] = {[capabilities.refresh.commands.refresh.NAME] = handle_refresh} - }, - supported_capabilities = { - capabilities.lock, - capabilities.lockUsers, - capabilities.lockCredentials, - capabilities.lockSchedules, - capabilities.battery, - capabilities.batteryLevel - }, - can_handle = require("new-matter-lock.can_handle"), -} - -return new_matter_lock_handler diff --git a/drivers/SmartThings/matter-lock/src/sub_drivers.lua b/drivers/SmartThings/matter-lock/src/sub_drivers.lua index 0f5d1f76d2..563c428945 100644 --- a/drivers/SmartThings/matter-lock/src/sub_drivers.lua +++ b/drivers/SmartThings/matter-lock/src/sub_drivers.lua @@ -3,6 +3,6 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { - lazy_load_if_possible("new-matter-lock"), + lazy_load_if_possible("unmigrated-matter-lock"), } return sub_drivers diff --git a/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua index bb7454d27d..2f3566eb6c 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua @@ -70,17 +70,15 @@ local function test_init() test.socket.matter:__expect_send({mock_device.id, req}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) - subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) - subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device)) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) - test.mock_device.add_test_device(mock_device_level) test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "added" }) test.socket.device_lifecycle:__queue_receive({ mock_device_level.id, "init" }) local subscribe_request_level = clusters.DoorLock.attributes.LockState:subscribe(mock_device_level) + subscribe_request_level:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device_level)) test.socket["matter"]:__expect_send({mock_device_level.id, subscribe_request_level}) end test.set_test_init_function(test_init) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua index 9693289951..58185f2ec8 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -42,7 +42,6 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) - subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) @@ -384,10 +383,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear()) ) -end, -{ - min_api_version = 17 -} + end, + { + min_api_version = 17 + } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua index 18bf34d3b9..e43e075baf 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua @@ -67,9 +67,7 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) - subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) - subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device)) 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({ provisioning_state = "PROVISIONED" }) @@ -88,9 +86,7 @@ local function test_init_no_battery() test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "init" }) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device_no_battery) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device_no_battery)) - subscribe_request:merge(clusters.DoorLock.events.DoorLockAlarm:subscribe(mock_device_no_battery)) subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device_no_battery)) - subscribe_request:merge(clusters.DoorLock.events.LockUserChange:subscribe(mock_device_no_battery)) test.socket["matter"]:__expect_send({mock_device_no_battery.id, subscribe_request}) test.socket.device_lifecycle:__queue_receive({ mock_device_no_battery.id, "doConfigure" }) mock_device_no_battery:expect_metadata_update({ profile = "base-lock-nobattery" }) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua index 8bb78f3fce..f1b3c2f182 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua @@ -39,6 +39,7 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = clusters.DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatChargeLevel:subscribe(mock_device)) + subscribe_request:merge(clusters.DoorLock.events.LockOperation:subscribe(mock_device)) 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({ provisioning_state = "PROVISIONED" }) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua index a0a603c6b3..c2e86aae29 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua @@ -45,9 +45,7 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) - subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) - subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) 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 = "base-lock-nobattery" }) @@ -910,4 +908,70 @@ test.register_coroutine_test( } ) +local enabled_optional_component_capability_pairs = {{ + "main", + { + capabilities.lockUsers.ID, + capabilities.lockCredentials.ID, + capabilities.battery.ID, + } +}} + +test.register_coroutine_test( + "Lock codes stored during migration", + function() + init_code_slot(1, "Code 1", mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.lockCodes.lockCodes( + json.encode({["1"] = "Code 1"}), {visibility = {displayed = false}} + ) + ) + ) + test.wait_for_events() + init_code_slot(2, "Code 2", mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.lockCodes.lockCodes( + json.encode({["1"] = "Code 1", ["2"] = "Code 2"}), {visibility = {displayed = false}} + ) + ) + ) + test.wait_for_events() + init_code_slot(3, "Code 3", mock_device) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.lockCodes.lockCodes( + json.encode({["1"] = "Code 1", ["2"] = "Code 2", ["3"] = "Code 3"}), + {visibility = {displayed = false}} + ) + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockCodes.ID, + command = "migrate", + args = {} + }, + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true)) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = enabled_optional_component_capability_pairs }) + + end, + { + min_api_version = 17 + } +) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua index 72d905d1db..39066776f9 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua @@ -44,9 +44,7 @@ local function test_init() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device)) - subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) - subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) test.socket.matter:__expect_send({mock_device.id, DoorLock.attributes.RequirePINforRemoteOperation:read(mock_device, 10)}) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_migration.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_migration.lua new file mode 100644 index 0000000000..b94b31e105 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_migration.lua @@ -0,0 +1,143 @@ +-- 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 clusters = require "st.matter.clusters" +local DoorLock = clusters.DoorLock +local lock_utils = require "lock_utils" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + manufacturer_info = { + vendor_id = 0x135D, + product_id = 0x00C1, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x0101, -- PIN & USR + } + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + -- mock_device:set_field(lock_utils.LOCK_CODES_FOR_MIGRATION, { {1, "ST Remote Operation Code"}, {2, "Guest1"}, {3, "Guest2"} }, {persist=true}) +end + +test.set_test_init_function(test_init) + +local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) +subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device)) +subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) +subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) +subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) + +local enabled_optional_component_capability_pairs = {{ + "main", + { + capabilities.lockUsers.ID, + capabilities.lockCredentials.ID, + capabilities.battery.ID, + } +}} + +test.register_coroutine_test( + "Migration completed and User and Credential values restored", + function() + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = capabilities.lockCodes.ID, command = "migrate", args = {}} + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true)) + ) + -- at this point, refresh is injected in migration. + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = enabled_optional_component_capability_pairs }) + + + test.wait_for_events() + -- assume this was set prior + mock_device:set_field(lock_utils.LOCK_CODES_FOR_MIGRATION, { {1, "ST Remote Operation Code"}, {2, "Guest1"}, {3, "Guest2"} }, {persist=true}) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed( + {profile = {id = "00000000-1111-2222-3333-000000000010", components = { main = {capabilities={ + ["lock"]= {id="lock", version=1}, ["lockAlarm"] = {id="lockAlarm", version=1}, ["remoteControlStatus"] = {id="remoteControlStatus", version=1}, + ["lockUsers"] = {id="lockUsers", version=1}, ["lockCredentials"] = {id="lockCredentials", version=1}, ["firmwareUpdate"] = {id="firmwareUpdate", version=1}, + ["refresh"] = {id="refresh", version=1}}}}}} + )) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + {userIndex=1, userName="ST Remote Operation Code", userType="guest"}, + {userIndex=2, userName="Guest1", userType="guest"}, + {userIndex=3, userName="Guest2", userType="guest"} + }, + {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { + {credentialIndex=1, credentialType="pin", userIndex=1}, + {credentialIndex=2, credentialType="pin", userIndex=2}, + {credentialIndex=3, credentialType="pin", userIndex=3} + }, + {visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua index 4b875fd1bc..745e769f9e 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua @@ -625,7 +625,7 @@ test.register_coroutine_test( local mock_nuki_smart_lock_ultra = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("lock-nocodes-notamper.yml"), + profile = t_utils.get_profile_definition("lock-unlatch.yml"), manufacturer_info = { vendor_id = 0x135D, product_id = 0x00A1, diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua index 492a3c2805..35acbfbff6 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua @@ -1,7 +1,7 @@ -- Copyright 2023 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 - +-- These are test units designed to validate User, Credential, and Schedule features. local test = require "integration_test" test.set_rpc_version(0) local capabilities = require "st.capabilities" @@ -13,7 +13,7 @@ local types = DoorLock.types local lock_utils = require "lock_utils" local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("lock-user-pin-schedule.yml"), + profile = t_utils.get_profile_definition("lock.yml"), manufacturer_info = { vendor_id = 0x115f, product_id = 0x2802, @@ -48,6 +48,12 @@ local mock_device = test.mock_device.build_test_matter_device({ local DoorLockFeatureMapAttr = {ID = 0xFFFC, cluster = DoorLock.ID} local function test_init() test.disable_startup_messages() + -- device_added + test.mock_device.add_test_device(mock_device) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) -- subscribe request local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) @@ -62,27 +68,23 @@ local function test_init() subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) - -- add test device, handle initial subscribe - test.mock_device.add_test_device(mock_device) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - -- actual onboarding flow - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) - ) + -- device_init test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + -- do_configure test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) ) mock_device:expect_metadata_update({ profile = "lock-user-pin-schedule" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + -- info_changed + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = t_utils.get_profile_definition("lock-user-pin-schedule.yml")})) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) end test.set_test_init_function(test_init) - test.register_coroutine_test( "Handle driverSwitched event", function() @@ -977,23 +979,6 @@ test.register_message_test( } ) -test.register_coroutine_test( - "Added lifecycle event lock nocodes nobattery", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockAlarm.alarm.clear({state_change = true}) - ) - ) - end, - { - min_api_version = 17 - } -) - test.register_coroutine_test( "Handle Add User command received from SmartThings.", function() @@ -2208,6 +2193,187 @@ test.register_coroutine_test( } ) +local LOCAL_START_TIME = "2025-01-15T09:00:00" +local LOCAL_END_TIME = "2025-01-15T17:00:00" + +test.register_coroutine_test( + "Handle Add Year Day Schedule command received from SmartThings.", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockSchedules.ID, + command = "setYearDaySchedule", + args = {1, 1, {localStartTime=LOCAL_START_TIME, localEndTime=LOCAL_END_TIME}} + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetYearDaySchedule( + mock_device, 1, -- endpoint + 1, -- Schedule Index + 1, -- User Index + lock_utils.iso8601_to_epoch(LOCAL_START_TIME), -- Start Time (epoch) + lock_utils.iso8601_to_epoch(LOCAL_END_TIME) -- End Time (epoch) + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.server.commands.SetYearDaySchedule:build_test_command_response( + mock_device, 1 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.yearDaySchedules( + {{ + userIndex=1, + schedules={{ + scheduleIndex = 1, + localStartTime = LOCAL_START_TIME, + localEndTime = LOCAL_END_TIME + }}, + }}, + {visibility={displayed=false}} + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.commandResult( + {commandName="setYearDaySchedule", userIndex=1, scheduleIndex=1, statusCode="success"}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Add Year Day Schedule command received from SmartThings and send busy state", + function() + mock_device:set_field(lock_utils.BUSY_STATE, os.time(), {persist = true}) + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockSchedules.ID, + command = "setYearDaySchedule", + args = {1, 1, {localStartTime=LOCAL_START_TIME, localEndTime=LOCAL_END_TIME}} + }, + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.commandResult( + {commandName="setYearDaySchedule", statusCode="busy"}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle Clear Year Day Schedule command received from SmartThings.", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockSchedules.ID, + command = "clearYearDaySchedules", + args = { + 1, -- user index + 1, -- schedule index + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetYearDaySchedule( + mock_device, 1, -- endpoint + 1, -- year_day_index + 1, -- user_index + 0, -- local_start_time + lock_utils.MAX_EPOCH_S -- local_end_time + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.server.commands.SetYearDaySchedule:build_test_command_response( + mock_device, 1 + ) + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.commandResult( + {commandName="clearYearDaySchedules", userIndex=1, scheduleIndex=1, statusCode="success"}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Clear Year Day Schedule command received from SmartThings and send busy state", + function() + mock_device:set_field(lock_utils.BUSY_STATE, os.time(), {persist = true}) + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockSchedules.ID, + command = "clearYearDaySchedules", + args = { + 1, -- user index + 1, -- schedule index + } + }, + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockSchedules.commandResult( + {commandName="clearYearDaySchedules", statusCode="busy"}, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Add Guest User and failure response ", function() @@ -2447,5 +2613,4 @@ test.register_coroutine_test( } ) - test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua index 166fa7110a..250fcb8af5 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua @@ -12,6 +12,30 @@ -- See the License for the specific language governing permissions and -- limitations under the License. +local FIXTURE_DER_B64 = + "MHcCAQEEIHlEZiE0cRiQ+Jp+RAGQ/Rq8miEBQXfRQNeSlyNR0Cv1oAoGCCqGSM49AwEH" .. + "oUQDQgAEeBuCTahXpt/rGLvVnOxjTlbmNYdKQF3vlHZYMK/LtISNUXJsJ1BfBX9nGgwY" .. + "WlJ775K5woV3zzMW7X4dVV5C7Q==" +local EXPECTED_PRIV_HEX = "7944662134711890f89a7e440190fd1abc9a21014177d140d792972351d02bf5" +local EXPECTED_PUB_HEX = "04781b824da857a6dfeb18bbd59cec634e56e635874a405def94765830afcbb48" .. + "48d51726c27505f057f671a0c185a527bef92b9c28577cf3316ed7e1d555e42ed" +local EXPECTED_GROUP_ID_HEX = "64c8cce93255c4478d7aa05d83f3eaa2" + +local base64 = require "base64" +local function default_generate_self_signed_cert(_opts) + local der = base64.decode(FIXTURE_DER_B64) + return { + cert_pem = "-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----", + key_pem = "-----BEGIN EC PRIVATE KEY-----\nfake\n-----END EC PRIVATE KEY-----", + cert_der = der, + key_der = der, + } +end +local security_stub = { + generate_self_signed_cert = default_generate_self_signed_cert, +} +package.loaded["st.security"] = security_stub + local test = require "integration_test" local capabilities = require "st.capabilities" local t_utils = require "integration_test.utils" @@ -19,6 +43,14 @@ local clusters = require "st.matter.clusters" local cluster_base = require "st.matter.cluster_base" local DoorLock = clusters.DoorLock local OctetString1 = require "st.matter.data_types.OctetString1" +local lock_utils = require "lock_utils" +lock_utils.create_group_id_resolving_key = function() + return EXPECTED_GROUP_ID_HEX +end + +local key_id = "vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=" +local endpoint_key = "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809" +local request_id = "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2" local enabled_optional_component_capability_pairs = {{ "main", @@ -65,6 +97,25 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local DoorLockFeatureMapAttr = {ID = 0xFFFC, cluster = DoorLock.ID} +local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) +subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.AliroReaderVerificationKey:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.AliroReaderGroupIdentifier:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.AliroReaderGroupSubIdentifier:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.AliroGroupResolvingKey:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.AliroBLEAdvertisingVersion:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfAliroEndpointKeysSupported:subscribe(mock_device)) +subscribe_request:merge(cluster_base.subscribe(mock_device, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) +subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) +subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) +subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) + local function test_init() test.disable_startup_messages() test.mock_device.add_test_device(mock_device) @@ -73,26 +124,7 @@ local function test_init() mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) ) test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) - local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) - subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfWeekDaySchedulesSupportedPerUser:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfYearDaySchedulesSupportedPerUser:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroReaderVerificationKey:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroReaderGroupIdentifier:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroReaderGroupSubIdentifier:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroExpeditedTransactionSupportedProtocolVersions:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroGroupResolvingKey:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroSupportedBLEUWBProtocolVersions:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.AliroBLEAdvertisingVersion:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfAliroCredentialIssuerKeysSupported:subscribe(mock_device)) - subscribe_request:merge(DoorLock.attributes.NumberOfAliroEndpointKeysSupported:subscribe(mock_device)) - subscribe_request:merge(cluster_base.subscribe(mock_device, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) - subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) - subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) - subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) @@ -113,17 +145,15 @@ test.register_coroutine_test( mock_device.id, DoorLock.attributes.AliroReaderVerificationKey:build_test_report_data( mock_device, 1, - "\x04\xA9\xCB\xE4\x18\xEB\x09\x66\x16\x43\xE2\xA4\xA8\x46\xB8\xED\xFE\x27\x86\x98\x30\x2E\x9F\xB4\x3E\x9B\xFF\xD3\xE3\x10\xCC\x2C\x2C\x7F\xF4\x02\xE0\x6E\x40\xEA\x3C\xE1\x29\x43\x52\x73\x36\x68\x3F\xC5\xB1\xCB\x0C\x6A\x7C\x3F\x0B\x5A\xFF\x78\x35\xDF\x21\xC6\x24" + lock_utils.hex_string_to_octet_string(EXPECTED_PUB_HEX) ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockAliro.readerVerificationKey( - "04a9cbe418eb09661643e2a4a846b8edfe278698302e9fb43e9bffd3e310cc2c2c7ff402e06e40ea3ce12943527336683fc5b1cb0c6a7c3f0b5aff7835df21c624", - {visibility = {displayed = false}}) - ) + capabilities.lockAliro.readerVerificationKey(EXPECTED_PUB_HEX, {visibility = {displayed = false}}) + ) ) end ) @@ -136,17 +166,15 @@ test.register_coroutine_test( mock_device.id, DoorLock.attributes.AliroReaderGroupIdentifier:build_test_report_data( mock_device, 1, - "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8" + lock_utils.hex_string_to_octet_string(EXPECTED_GROUP_ID_HEX) ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockAliro.readerGroupIdentifier( - "e24f1b205ba923b32cd13dc009e993a8", - {visibility = {displayed = false}}) - ) + capabilities.lockAliro.readerGroupIdentifier(EXPECTED_GROUP_ID_HEX, {visibility = {displayed = false}}) + ) ) end ) @@ -228,17 +256,15 @@ test.register_coroutine_test( mock_device.id, DoorLock.attributes.AliroGroupResolvingKey:build_test_report_data( mock_device, 1, - "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8" + lock_utils.hex_string_to_octet_string(EXPECTED_GROUP_ID_HEX) ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", - capabilities.lockAliro.groupResolvingKey( - "e24f1b205ba923b32cd13dc009e993a8", - {visibility = {displayed = false}}) - ) + capabilities.lockAliro.groupResolvingKey(EXPECTED_GROUP_ID_HEX, {visibility = {displayed = false}}) + ) ) end ) @@ -321,9 +347,9 @@ test.register_coroutine_test( capability = capabilities.lockAliro.ID, command = "setReaderConfig", args = { - "1a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac", - "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", - "e24f1b205ba923b32cd13dc009e993a8", + EXPECTED_PRIV_HEX, + EXPECTED_PUB_HEX, + EXPECTED_GROUP_ID_HEX, nil } }, @@ -334,9 +360,9 @@ test.register_coroutine_test( mock_device.id, DoorLock.server.commands.SetAliroReaderConfig( mock_device, 1, -- endpoint - "\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC", - "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", - "\xE2\x4F\x1B\x20\x5B\xA9\x23\xB3\x2C\xD1\x3D\xC0\x09\xE9\x93\xA8", + lock_utils.hex_string_to_octet_string(EXPECTED_PRIV_HEX), + lock_utils.hex_string_to_octet_string(EXPECTED_PUB_HEX), + lock_utils.hex_string_to_octet_string(EXPECTED_GROUP_ID_HEX), nil ), } @@ -373,11 +399,11 @@ test.register_coroutine_test( capability = capabilities.lockAliro.ID, command = "setEndpointKey", args = { - 0, - "vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", + 0, -- user index + key_id, "nonEvictableEndpointKey", - "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", - "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2" + endpoint_key, + request_id } }, } @@ -394,7 +420,7 @@ test.register_coroutine_test( credential_index = 1 } ), -- credential - "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", -- credential_data + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data nil, -- user_index nil, -- user_status DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type @@ -427,7 +453,7 @@ test.register_coroutine_test( "main", capabilities.lockAliro.credentials( {{ - keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", + keyId=key_id, keyIndex=1, keyType="nonEvictableEndpointKey", userIndex=1 @@ -442,8 +468,8 @@ test.register_coroutine_test( capabilities.lockAliro.commandResult( { commandName="setEndpointKey", - keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", - requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", + keyId=key_id, + requestId=request_id, statusCode="success", userIndex=1 }, @@ -458,7 +484,7 @@ test.register_coroutine_test( { capability = capabilities.lockAliro.ID, command = "clearEndpointKey", - args = {1, "vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", "nonEvictableEndpointKey"} + args = {1, key_id, "nonEvictableEndpointKey"} }, } ) @@ -512,7 +538,7 @@ test.register_coroutine_test( capabilities.lockAliro.commandResult( { commandName="clearEndpointKey", - keyId="vTNt0oPoHvIvwGMHa3AuXE3ZcY+Oocv5KZ+R0yveEag=", + keyId=key_id, statusCode="success", userIndex=1 }, @@ -523,6 +549,261 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle Set Endpoint Key command received from SmartThings and busy status", + function() + lock_utils.is_busy_state_set(mock_device) + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setEndpointKey", + args = { + 0, -- user index + key_id, + "nonEvictableEndpointKey", + endpoint_key, + request_id + } + }, + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setEndpointKey", + keyId=key_id, + requestId=request_id, + statusCode="busy" + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle Set Endpoint Key command received from SmartThings and user_index is occupied", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setEndpointKey", + args = { + 0, -- user index + key_id, + "nonEvictableEndpointKey", + endpoint_key, + request_id + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY, + credential_index = 1 + } + ), -- credential + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.OCCUPIED, -- status + 1, -- user_index + 2 -- next_credential_index + ), + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY, + credential_index = 2 + } + ), -- credential + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle Set Endpoint Key command received from SmartThings and user_index is occupied and next_credential_index is nil", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setEndpointKey", + args = { + 0, -- user index + key_id, + "nonEvictableEndpointKey", + endpoint_key, + request_id + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY, + credential_index = 1 + } + ), -- credential + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.OCCUPIED, -- status + 1, -- user_index + nil -- next_credential_index + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setEndpointKey", + keyId=key_id, + requestId=request_id, + statusCode="resourceExhausted" + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle Set Endpoint Key command received from SmartThings and user_index is failure", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setEndpointKey", + args = { + 0, -- user index + key_id, + "nonEvictableEndpointKey", + endpoint_key, + request_id + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_NON_EVICTABLE_ENDPOINT_KEY, + credential_index = 1 + } + ), -- credential + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.FAILURE, -- status + 1, -- user_index + 2 -- next_credential_index + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setEndpointKey", + keyId=key_id, + requestId=request_id, + statusCode="failure" + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + test.register_coroutine_test( "Handle Set Issuer Key command and Clear Issuer Key command received from SmartThings.", function() @@ -534,8 +815,8 @@ test.register_coroutine_test( command = "setIssuerKey", args = { 0, - "041a748a78566aaee985d9141730fa72bd83bf34e7b93072a0ca7b56a79b6debac9493eded05a65701b5148517bd49a6c91c78ed6811543491eff1d257280ed809", - "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2" + endpoint_key, + request_id } }, } @@ -552,7 +833,7 @@ test.register_coroutine_test( credential_index = 1 } ), -- credential - "\x04\x1A\x74\x8A\x78\x56\x6A\xAE\xE9\x85\xD9\x14\x17\x30\xFA\x72\xBD\x83\xBF\x34\xE7\xB9\x30\x72\xA0\xCA\x7B\x56\xA7\x9B\x6D\xEB\xAC\x94\x93\xED\xED\x05\xA6\x57\x01\xB5\x14\x85\x17\xBD\x49\xA6\xC9\x1C\x78\xED\x68\x11\x54\x34\x91\xEF\xF1\xD2\x57\x28\x0E\xD8\x09", -- credential_data + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data nil, -- user_index nil, -- user_status DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type @@ -599,7 +880,7 @@ test.register_coroutine_test( capabilities.lockAliro.commandResult( { commandName="setIssuerKey", - requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", + requestId=request_id, statusCode="success", userIndex=1 }, @@ -614,7 +895,7 @@ test.register_coroutine_test( { capability = capabilities.lockAliro.ID, command = "clearIssuerKey", - args = {1, "1f3acdf6-8930-45f7-ae3d-f0b47851c3e2"} + args = {1, request_id} }, } ) @@ -668,7 +949,7 @@ test.register_coroutine_test( capabilities.lockAliro.commandResult( { commandName="clearIssuerKey", - requestId="1f3acdf6-8930-45f7-ae3d-f0b47851c3e2", + requestId=request_id, statusCode="success", userIndex=1 }, @@ -679,4 +960,304 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Handle Set Issuer Key command received from SmartThings and busy status", + function() + lock_utils.is_busy_state_set(mock_device) + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setIssuerKey", + args = { + 0, + endpoint_key, + request_id + } + }, + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setIssuerKey", + requestId=request_id, + statusCode="busy" + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle Set Issuer Key command received from SmartThings and user_index is occupied", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setIssuerKey", + args = { + 0, + endpoint_key, + request_id + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = 1 + } + ), -- credential + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.OCCUPIED, -- status + 1, -- user_index + 2 -- next_credential_index + ), + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = 2 + } + ), -- credential + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle Set Issuer Key command received from SmartThings and user_index is occupied and next_credential_index is nil", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setIssuerKey", + args = { + 0, + endpoint_key, + request_id + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = 1 + } + ), -- credential + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.OCCUPIED, -- status + 1, -- user_index + nil -- next_credential_index + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setIssuerKey", + requestId=request_id, + statusCode="resourceExhausted" + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle Set Issuer Key command received from SmartThings and user_index is failure", + function() + test.socket.capability:__queue_receive( + { + mock_device.id, + { + capability = capabilities.lockAliro.ID, + command = "setIssuerKey", + args = { + 0, + endpoint_key, + request_id + } + }, + } + ) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetCredential( + mock_device, 1, -- endpoint + DoorLock.types.DataOperationTypeEnum.ADD, -- operation_type + DoorLock.types.CredentialStruct( + { + credential_type = DoorLock.types.CredentialTypeEnum.ALIRO_CREDENTIAL_ISSUER_KEY, + credential_index = 1 + } + ), -- credential + lock_utils.hex_string_to_octet_string(endpoint_key), -- credential_data + nil, -- user_index + nil, -- user_status + DoorLock.types.DlUserType.UNRESTRICTED_USER -- user_type + ), + } + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + DoorLock.client.commands.SetCredentialResponse:build_test_command_response( + mock_device, 1, + DoorLock.types.DlStatus.FAILURE, -- status + 1, -- user_index + 2 -- next_credential_index + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setIssuerKey", + requestId=request_id, + statusCode="failure" + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "set_reader_config should send SetAliroReaderConfig command on device init", + function() + mock_device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, nil, {persist = true}) + mock_device:set_field(lock_utils.BUSY_STATE, false, {persist = true}) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = t_utils.get_profile_definition("lock.yml")})) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.socket.matter:__expect_send( + { + mock_device.id, + DoorLock.server.commands.SetAliroReaderConfig( + mock_device, 1, + lock_utils.hex_string_to_octet_string(EXPECTED_PRIV_HEX), + lock_utils.hex_string_to_octet_string(EXPECTED_PUB_HEX), + lock_utils.hex_string_to_octet_string(EXPECTED_GROUP_ID_HEX), + nil + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) + ) + end +) + +test.register_coroutine_test( + "Set Reader Config command sets busy state in command result when busy", + function() + mock_device:set_field(lock_utils.ALIRO_READER_CONFIG_UPDATED, nil, {persist = true}) + lock_utils.is_busy_state_set(mock_device) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = t_utils.get_profile_definition("lock.yml")})) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockAliro.commandResult( + { + commandName="setReaderConfig", + statusCode="busy" + }, + {state_change=true, visibility={displayed=false}} + ) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) + ) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua index 64f8944a43..8ec2a2a781 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua @@ -162,6 +162,82 @@ local mock_device_user_pin_schedule_unlatch = test.mock_device.build_test_matter } }) +local mock_device_battery = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-battery.yml"), + manufacturer_info = { + vendor_id = 0x147F, + product_id = 0x0001, + }, + 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 = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x0, + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local mock_device_battery_level = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("lock-batteryLevel.yml"), + manufacturer_info = { + vendor_id = 0x147F, + product_id = 0x0001, + }, + 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 = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x0, + }, + { + cluster_id = clusters.PowerSource.ID, + cluster_type = "SERVER", + feature_map = 10 + }, + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + local DoorLockFeatureMapAttr = {ID = 0xFFFC, cluster = DoorLock.ID} local function test_init() test.disable_startup_messages() @@ -273,6 +349,56 @@ local function test_init_user_pin_schedule_unlatch() mock_device_user_pin_schedule_unlatch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end +local function test_init_battery() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_battery) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_battery)) + subscribe_request:merge(cluster_base.subscribe(mock_device_battery, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_battery)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_battery)) + subscribe_request:merge(clusters.PowerSource.attributes.BatPercentRemaining:subscribe(mock_device_battery)) + subscribe_request:merge(clusters.PowerSource.attributes.AttributeList:subscribe(mock_device_battery)) + + -- add test device, handle initial subscribe + test.mock_device.add_test_device(mock_device_battery) + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "added" }) + test.socket.capability:__expect_send( + mock_device_battery:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "init" }) + test.socket.matter:__expect_send({mock_device_battery.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery.id, "doConfigure" }) + mock_device_battery:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + +local function test_init_battery_level() + test.disable_startup_messages() + -- subscribe request + local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device_battery_level) + subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device_battery_level)) + subscribe_request:merge(cluster_base.subscribe(mock_device_battery_level, nil, DoorLockFeatureMapAttr.cluster, DoorLockFeatureMapAttr.ID)) + subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device_battery_level)) + subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device_battery_level)) + subscribe_request:merge(clusters.PowerSource.attributes.BatChargeLevel:subscribe(mock_device_battery_level)) + subscribe_request:merge(clusters.PowerSource.attributes.AttributeList:subscribe(mock_device_battery_level)) + + -- add test device, handle initial subscribe + test.mock_device.add_test_device(mock_device_battery_level) + test.socket.matter:__expect_send({mock_device_battery_level.id, subscribe_request}) + -- actual onboarding flow + test.socket.device_lifecycle:__queue_receive({ mock_device_battery_level.id, "added" }) + test.socket.capability:__expect_send( + mock_device_battery_level:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery_level.id, "init" }) + test.socket.matter:__expect_send({mock_device_battery_level.id, subscribe_request}) + test.socket.device_lifecycle:__queue_receive({ mock_device_battery_level.id, "doConfigure" }) + mock_device_battery_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + test.set_test_init_function(test_init) test.register_coroutine_test( @@ -656,4 +782,68 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "Handle received BatPercentRemaining from device.", + function() + test.socket.matter:__queue_receive( + { + mock_device_battery.id, + clusters.PowerSource.attributes.BatPercentRemaining:build_test_report_data( + mock_device_battery, 1, 150 + ) + } + ) + test.socket.capability:__expect_send( + mock_device_battery:generate_test_message("main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5))) + ) + end, + { + test_init = test_init_battery, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Handle received BatChargeLevel from device.", + function() + test.socket.matter:__queue_receive( + { + mock_device_battery_level.id, + clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( + mock_device_battery_level, 1, clusters.PowerSource.types.BatChargeLevelEnum.OK + ) + } + ) + test.socket.capability:__expect_send( + mock_device_battery_level:generate_test_message("main", capabilities.batteryLevel.battery.normal()) + ) + test.socket.matter:__queue_receive( + { + mock_device_battery_level.id, + clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( + mock_device_battery_level, 1, clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL + ) + } + ) + test.socket.capability:__expect_send( + mock_device_battery_level:generate_test_message("main", capabilities.batteryLevel.battery.critical()) + ) + test.socket.matter:__queue_receive( + { + mock_device_battery_level.id, + clusters.PowerSource.attributes.BatChargeLevel:build_test_report_data( + mock_device_battery_level, 1, clusters.PowerSource.types.BatChargeLevelEnum.WARNING + ) + } + ) + test.socket.capability:__expect_send( + mock_device_battery_level:generate_test_message("main", capabilities.batteryLevel.battery.warning()) + ) + end, + { + test_init = test_init_battery_level, + min_api_version = 17 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/unmigrated-matter-lock/can_handle.lua b/drivers/SmartThings/matter-lock/src/unmigrated-matter-lock/can_handle.lua new file mode 100644 index 0000000000..3da6b948bf --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/unmigrated-matter-lock/can_handle.lua @@ -0,0 +1,17 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local capabilities = require "st.capabilities" + +local function is_unmigrated_matter_lock_product(opts, driver, device) + local is_migrated = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.migrated.NAME) or nil + if device:supports_capability(capabilities.lockAlarm) then + return false + elseif device:supports_capability(capabilities.lockCodes) and is_migrated == true then + return false + else + return true, require("unmigrated-matter-lock") + end +end + +return is_unmigrated_matter_lock_product diff --git a/drivers/SmartThings/matter-lock/src/unmigrated-matter-lock/init.lua b/drivers/SmartThings/matter-lock/src/unmigrated-matter-lock/init.lua new file mode 100644 index 0000000000..6e7d55d895 --- /dev/null +++ b/drivers/SmartThings/matter-lock/src/unmigrated-matter-lock/init.lua @@ -0,0 +1,758 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local clusters = require "st.matter.clusters" +local DoorLock = clusters.DoorLock +local PowerSource = clusters.PowerSource +local capabilities = require "st.capabilities" +local im = require "st.matter.interaction_model" +local lock_utils = require "lock_utils" + +local PROFILE_BASE_NAME = "__profile_base_name" +local INITIAL_COTA_INDEX = 1 +-- add this definition for locks to work on older lua libs +local UNLATCHED_STATE = 0x3 + +local subscribed_attributes = { + [capabilities.lock.ID] = { + DoorLock.attributes.LockState + }, + [capabilities.battery.ID] = { + PowerSource.attributes.BatPercentRemaining + }, + [capabilities.batteryLevel.ID] = { + PowerSource.attributes.BatChargeLevel + } +} + +--- If a device needs a cota credential this function attempts to set the credential +--- at the index provided. The set_credential_response_handler handles all failures +--- and retries with the appropriate index when necessary. +local function set_cota_credential(device, credential_index) + local eps = device:get_endpoints(DoorLock.ID) + local cota_cred = device:get_field(lock_utils.COTA_CRED) + if cota_cred == nil then + -- Shouldn't happen but defensive to try to figure out if we need the cota cred and set it. + device:send(DoorLock.attributes.RequirePINforRemoteOperation:read(device, #eps > 0 and eps[1] or 1)) + return + elseif cota_cred == false then + device.log.debug("Device does not require PIN for remote operation. Not setting COTA credential") + return + end + + if device:get_field(lock_utils.SET_CREDENTIAL) ~= nil then + device.log.debug("delaying setting COTA credential since a credential is currently being set") + device.thread:call_with_delay(2, function(t) + set_cota_credential(device, credential_index) + end) + return + end + + device:set_field(lock_utils.COTA_CRED_INDEX, credential_index, {persist = true}) + local credential = {credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = credential_index} + -- Set the credential to a code + device:set_field(lock_utils.SET_CREDENTIAL, credential_index) + device.log.info(string.format("Attempting to set COTA credential at index %s", credential_index)) + device:send(DoorLock.server.commands.SetCredential( + device, + #eps > 0 and eps[1] or 1, + DoorLock.types.DlDataOperationType.ADD, + credential, + device:get_field(lock_utils.COTA_CRED), + nil, -- nil user_index creates a new user + DoorLock.types.DlUserStatus.OCCUPIED_ENABLED, + DoorLock.types.DlUserType.REMOTE_ONLY_USER + )) +end + +local function generate_cota_cred_for_device(device) + local len = device:get_latest_state("main", capabilities.lockCodes.ID, capabilities.lockCodes.maxCodeLength.NAME) or 4 + local cred_data = math.floor(math.random() * (10 ^ len)) + cred_data = string.format("%0" .. tostring(len) .. "d", cred_data) + device:set_field(lock_utils.COTA_CRED, cred_data, {persist = true}) +end + +local function lock_state_handler(driver, device, ib, response) + local LockState = DoorLock.attributes.LockState + local attr = capabilities.lock.lock + local LOCK_STATE = { + [LockState.NOT_FULLY_LOCKED] = attr.not_fully_locked(), + [LockState.LOCKED] = attr.locked(), + [LockState.UNLOCKED] = attr.unlocked(), + [UNLATCHED_STATE] = attr.unlocked(), -- Fully unlocked with latch pulled + } + + if ib.data.value ~= nil then + local event = LOCK_STATE[ib.data.value] + if event ~= nil then + device:emit_event(event) + else + device.log.warn(string.format("Received unknown lock state value (%s), emitting unknown", ib.data.value)) + device:emit_event(attr.unknown()) + end + else + device:emit_event(LOCK_STATE[LockState.NOT_FULLY_LOCKED]) + end +end + +local function handle_battery_percent_remaining(driver, device, ib, response) + if ib.data.value ~= nil then + device:emit_event(capabilities.battery.battery(math.floor(ib.data.value / 2.0 + 0.5))) + end +end + +local function handle_battery_charge_level(driver, device, ib, response) + if ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.OK then + device:emit_event(capabilities.batteryLevel.battery.normal()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.WARNING then + device:emit_event(capabilities.batteryLevel.battery.warning()) + elseif ib.data.value == clusters.PowerSource.types.BatChargeLevelEnum.CRITICAL then + device:emit_event(capabilities.batteryLevel.battery.critical()) + end +end + +local function handle_power_source_attribute_list(driver, device, ib, response) + local support_battery_percentage = false + local support_battery_level = false + for _, attr in ipairs(ib.data.elements) do + -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) or + -- BatChargeLevel (Attribute ID 0x0E) is present. + if attr.value == 0x0C then + support_battery_percentage = true + elseif attr.value == 0x0E then + support_battery_level = true + end + end + local profile_name = device:get_field(PROFILE_BASE_NAME) + if profile_name ~= nil then + if not support_battery_percentage then + if support_battery_level then + profile_name = profile_name .. "-batteryLevel" + else + profile_name = profile_name .. "-nobattery" + end + end + device.log.info(string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + end +end + +local function max_pin_code_len_handler(driver, device, ib, response) + device:emit_event(capabilities.lockCodes.maxCodeLength(ib.data.value, {visibility = {displayed = false}})) +end + +local function min_pin_code_len_handler(driver, device, ib, response) + device:emit_event(capabilities.lockCodes.minCodeLength(ib.data.value, {visibility = {displayed = false}})) +end + +local function num_pin_users_handler(driver, device, ib, response) + device:set_field(lock_utils.TOTAL_PIN_USERS, ib.data.value) + device:emit_event(capabilities.lockCodes.maxCodes(ib.data.value, {visibility = {displayed = false}})) +end + +local function apply_cota_credentials_if_absent(device) + if not device:get_field(lock_utils.COTA_CRED) then + --Process after all other info blocks have been dispatched to ensure MaxPINCodeLength has been processed + device.thread:call_with_delay(0, function(t) + generate_cota_cred_for_device(device) + -- delay needed to allow test to override the random credential data + device.thread:call_with_delay(0, function(t) + -- Attempt to set cota credential at the lowest index + set_cota_credential(device, INITIAL_COTA_INDEX) + end) + end) + end +end + +local function require_remote_pin_handler(driver, device, ib, response) + if ib.data.value then + apply_cota_credentials_if_absent(device) + else + device:set_field(lock_utils.COTA_CRED, false, {persist = true}) + end +end + +local function clear_credential_response_handler(driver, device, ib, response) + local deleted_code_slot = device:get_field(lock_utils.DELETING_CODE) + if deleted_code_slot == nil and ib.status == im.InteractionResponse.Status.SUCCESS then + device.log.debug("Cleared space in lock credential db for COTA credential") + return + end + if ib.status == im.InteractionResponse.Status.SUCCESS then + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, tostring(deleted_code_slot))) + --make sure cota credential exists if the user deletes it or if space was created for the COTA cred + if deleted_code_slot == device:get_field(lock_utils.COTA_CRED_INDEX) or + device:get_field(lock_utils.NONFUNCTIONAL) then + set_cota_credential(device, device:get_field(lock_utils.COTA_CRED_INDEX) or INITIAL_COTA_INDEX) + end + else + device.log.error(string.format("Failed to delete code slot %s", deleted_code_slot)) + end + device:set_field(lock_utils.DELETING_CODE, nil) +end + +local function set_credential_response_handler(driver, device, ib, response) + if ib.status ~= im.InteractionResponse.Status.SUCCESS then + device.log.error("Failed to set code for device") + return + end + local elements = ib.info_block.data.elements + local credential_index = device:get_field(lock_utils.SET_CREDENTIAL) + device:set_field(lock_utils.SET_CREDENTIAL, nil) + if credential_index == nil then + device.log.error("Received unexpected SetCredentialResponse") + return + end + local code_slot = tostring(credential_index) + local status = elements.status.value + if status == DoorLock.types.DlStatus.SUCCESS then + local event = capabilities.lockCodes.codeChanged("", {state_change = true}) + local cota_cred_index = device:get_field(lock_utils.COTA_CRED_INDEX) + local code_name = (credential_index == cota_cred_index and lock_utils.COTA_CODE_NAME) or + lock_utils.get_code_name(device, code_slot) + event.data = {codeName = code_name} + event.value = lock_utils.get_change_type(device, tostring(code_slot)) + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = event.data.codeName + device:emit_event(event) + if credential_index == cota_cred_index then + device:emit_event( + capabilities.lockCodes.codeChanged( + code_slot .. " renamed", {state_change = true} + ) + ) + end + lock_utils.lock_codes_event(device, lock_codes) + lock_utils.reset_code_state(device, code_slot) + if device:get_field(lock_utils.NONFUNCTIONAL) and cota_cred_index == credential_index then + device.log.info("Successfully set COTA credential after being non-functional") + device:set_field(lock_utils.NONFUNCTIONAL, false, {persist = true}) + local profile_name = "base-lock" + device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps == 0 then + profile_name = profile_name .. "-nobattery" + device.log.info(string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name, provisioning_state = "PROVISIONED"}) + else + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + req:merge(clusters.PowerSource.attributes.AttributeList:read()) + device:send(req) + end + end + elseif device:get_field(lock_utils.COTA_CRED) and credential_index == device:get_field(lock_utils.COTA_CRED_INDEX) then + -- Handle failure to set a COTA credential + if status == DoorLock.types.DlStatus.OCCUPIED and elements.next_credential_index.value ~= nil then + --This credential index is unavailable, but there is another available + set_cota_credential(device, elements.next_credential_index.value) + elseif status == DoorLock.types.DlStatus.OCCUPIED and + elements.next_credential_index.value == nil and + credential_index == INITIAL_COTA_INDEX then + --There are no credential indices available on the device + device.log.error("Device requires COTA credential, but has no credential indexes available!") + device.log.error("Lock and Unlock commands will no longer work!!") + device:set_field(PROFILE_BASE_NAME, "nonfunctional-lock", {persist = true}) + device:try_update_metadata({profile = "nonfunctional-lock", provisioning_state = "NONFUNCTIONAL"}) + device:set_field(lock_utils.NONFUNCTIONAL, true, {persist = true}) + elseif status == DoorLock.types.DlStatus.OCCUPIED and elements.next_credential_index.value == nil then + --There are no credential indices available, but we must ensure we search all indices. + set_cota_credential(device, INITIAL_COTA_INDEX) + elseif status == DoorLock.types.DlStatus.DUPLICATE then + --The credential we randomly generated already exists + generate_cota_cred_for_device(device) + --delay 0 needed for unit test verification of random value + device.thread:call_with_delay(0, function(t) set_cota_credential(device, credential_index) end) + elseif status == DoorLock.types.DlStatus.INVALID_FIELD then + device.log.error("Invalid SetCredential command sent to set a COTA credential. This is a bug.") + elseif elements.next_credential_index.value ~= nil then + device.log.warn(string.format( + "Received non-success SetCredentialResponse status (%s), but there is a next credential index available", elements.status + )) + set_cota_credential(device, elements.next_credential_index.value) + end + else + device.log.error( + string.format( + "Failed to set user code for device, SetCredential status received: %s", elements.status + ) + ) + end +end + +local function get_credential_status_response_handler(driver, device, ib, response) + if ib.status ~= im.InteractionResponse.Status.SUCCESS then + device.log.warn("Not taking action on GetCredentialStatusResponse because failed status") + end + local cred_index = device:get_field(lock_utils.CHECKING_CREDENTIAL) + if cred_index == nil then + device.log.warn("Received unexpected CredentialStatusResponse") + return + end + local elements = ib.info_block.data.elements + local credential_exists = elements.credential_exists.value + local next_credential_index = elements.next_credential_index and elements.next_credential_index.value or nil + + local event = capabilities.lockCodes.codeChanged("", {state_change = true}) + local code_slot = tostring(cred_index) + local cota_cred_index = device:get_field(lock_utils.COTA_CRED_INDEX) + local code_name = (cred_index == cota_cred_index and lock_utils.COTA_CODE_NAME) or lock_utils.get_code_name(device, code_slot) + event.data = {codeName = code_name} + if credential_exists then + -- Code slot is occupied + event.value = lock_utils.get_change_type(device, code_slot) + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = event.data.codeName + device:emit_event(event) + lock_utils.lock_codes_event(device, lock_codes) + lock_utils.reset_code_state(device, code_slot) + else + -- Code slot is unoccupied + if (lock_utils.get_lock_codes(device)[code_slot] ~= nil) then + -- Code has been deleted + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, code_slot)) + if cred_index == cota_cred_index then --make sure cota credential exists if it was deleted + set_cota_credential(device, INITIAL_COTA_INDEX) + end + else + -- Code is unset + event.value = code_slot .. " unset" + device:emit_event(event) + end + end + device:set_field(lock_utils.CHECKING_CREDENTIAL, nil) + + local is_scanning = device:get_latest_state( + "main", capabilities.lockCodes.ID, capabilities.lockCodes.scanCodes.NAME + ) == "Scanning" + if not is_scanning then + return + end + if (next_credential_index == nil) then + device:emit_event( + capabilities.lockCodes.scanCodes( + "Complete", {visibility = {displayed = false}} + ) + ) + local lock_codes = lock_utils.get_lock_codes(device) + lock_utils.lock_codes_event(device, lock_codes) + elseif next_credential_index ~= nil then + device:set_field(lock_utils.CHECKING_CREDENTIAL, next_credential_index) + device:send( + DoorLock.server.commands.GetCredentialStatus( + device, + ib.info_block.endpoint_id, + {credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = device:get_field(lock_utils.CHECKING_CREDENTIAL)} + ) + ) + end +end + +local function alarm_event_handler(driver, device, ib, response) + local DlAlarmCode = DoorLock.types.DlAlarmCode + local alarm_code = ib.data.elements.alarm_code + if alarm_code.value == DlAlarmCode.FRONT_ESCEUTCHEON_REMOVED or alarm_code.value + == DlAlarmCode.WRONG_CODE_ENTRY_LIMIT or alarm_code.value == DlAlarmCode.FORCED_USER + or alarm_code.value == DlAlarmCode.DOOR_FORCED_OPEN then + device:emit_event(capabilities.tamperAlert.tamper.detected()) + end +end + +local function lock_op_event_handler(driver, device, ib, response) + local tamper_detected = device:get_latest_state( + device:endpoint_to_component(ib.endpoint_id), + capabilities.tamperAlert.ID, capabilities.tamperAlert.tamper.NAME + ) + if nil == tamper_detected or tamper_detected == capabilities.tamperAlert.tamper.detected.NAME then + device:emit_event(capabilities.tamperAlert.tamper.clear()) + end +end + +local function lock_user_change_event_handler(driver, device, ib, response) + local event = capabilities.lockCodes.codeChanged("", {state_change = true}) + local elements = ib.data.elements + local data_type_changed = elements.lock_data_type.value + local operation_type = elements.data_operation_type.value + local user_index = elements.user_index.value + local data_index = elements.data_index and elements.data_index.value + local cota_cred_index = device:get_field(lock_utils.COTA_CRED_INDEX) + + if data_type_changed == DoorLock.types.DlLockDataType.PIN then -- pin added or removed + local code_slot = data_index and tostring(data_index) or nil + if (operation_type == DoorLock.types.DlDataOperationType.ADD or operation_type + == DoorLock.types.DlDataOperationType.MODIFY) and code_slot ~= nil then + local change_type = lock_utils.get_change_type(device, code_slot) + event.value = change_type + local code_name = (data_index == cota_cred_index and lock_utils.COTA_CODE_NAME) or lock_utils.get_code_name(device, code_slot) + event.data = {codeName = code_name} + device:emit_event(event) + if string.match(change_type, "%d+ set") ~= nil then + local lock_codes = lock_utils.get_lock_codes(device) + lock_codes[code_slot] = code_name + lock_utils.lock_codes_event(device, lock_codes) + end + elseif operation_type == DoorLock.types.DlDataOperationType.CLEAR and code_slot ~= nil then + lock_utils.lock_codes_event(device, lock_utils.code_deleted(device, tostring(code_slot))) + --make sure cota credential is created if the user deletes it or a space is made for it + if data_index == cota_cred_index or device:get_field(lock_utils.NONFUNCTIONAL) then + set_cota_credential(device, cota_cred_index or INITIAL_COTA_INDEX) + end + else -- invalid event because no credential index + device.log.error( + "Received unhandled LockUserChangeEvent because it didn't affect a PIN credential" + ) + end + elseif data_type_changed == DoorLock.types.DlLockDataType.USER_INDEX and operation_type + == DoorLock.types.DlDataOperationType.CLEAR then + if user_index == 0xFFFE then + device.log.warn("All users were cleared by another fabric") -- we never do this + for cs, _ in pairs(lock_utils.get_lock_codes(device)) do + lock_utils.code_deleted(device, cs) + end + lock_utils.lock_codes_event(device, {}) + if device:get_field(lock_utils.COTA_CRED) ~= nil then set_cota_credential(device, INITIAL_COTA_INDEX) end + else + device.log.info("Not handling LockUserChange event") + end + -- Note when a Lock User is deleted, the credentials associated with that user are also deleted. + -- Change events are created for each credential as well as the user. + else + device.log.info( + string.format( + "Not handling LockUserChange event because the data type (%s) doesn't affect lock codes", + elements.lock_data_type + ) + ) + end +end + +local function handle_refresh(driver, device, command) + -- Note: no endpoint specified indicates a wildcard endpoint + local req = DoorLock.attributes.LockState:read(device) + req:merge(PowerSource.attributes.BatPercentRemaining:read(device)) + device:send(req) +end + +local function handle_lock(driver, device, command) + local ep = device:component_to_endpoint(command.component) + local cota_cred = device:get_field(lock_utils.COTA_CRED) + if cota_cred then + device:send( + DoorLock.server.commands.LockDoor(device, ep, cota_cred) + ) + else + device:send(DoorLock.server.commands.LockDoor(device, ep)) + end +end + +local function handle_unlock(driver, device, command) + local ep = device:component_to_endpoint(command.component) + local cota_cred = device:get_field(lock_utils.COTA_CRED) + if cota_cred then + device:send( + DoorLock.server.commands.UnlockDoor(device, ep, cota_cred) + ) + else + device:send(DoorLock.server.commands.UnlockDoor(device, ep)) + end +end + +local function handle_delete_code(driver, device, command) + local endpoint = device:component_to_endpoint(command.component) + device:set_field(lock_utils.DELETING_CODE, command.args.codeSlot) + device:send(DoorLock.server.commands.ClearCredential( + device, + endpoint, + {credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = command.args.codeSlot} + )) +end + +local function handle_reload_all_codes(driver, device, command) + if (device:get_field(lock_utils.CHECKING_CREDENTIAL) == nil) then + lock_utils.lock_codes_event(device, {}) + device:set_field(lock_utils.CHECKING_CREDENTIAL, 1) + else + device.log.info(string.format("Delaying scanning since currently checking credential %d", device:get_field(lock_utils.CHECKING_CREDENTIAL))) + device.thread:call_with_delay(2, function(t) handle_reload_all_codes(driver, device, command) end) + return + end + device.thread:call_with_delay(5, function(t) + device:emit_event(capabilities.lockCodes.scanCodes("Scanning")) + device:send( + clusters.DoorLock.server.commands.GetCredentialStatus( + device, device:component_to_endpoint(command.component), + {credential_type = DoorLock.types.DlCredentialType.PIN, credential_index = device:get_field(lock_utils.CHECKING_CREDENTIAL)} + ) + ) + end) +end + +local function handle_request_code(driver, device, command) + local endpoint_id = device:component_to_endpoint(command.component) + device:set_field(lock_utils.CHECKING_CREDENTIAL, command.args.codeSlot) + local credential = { + credential_type = DoorLock.types.DlCredentialType.PIN, + credential_index = command.args.codeSlot, + } + device:send(clusters.DoorLock.server.commands.GetCredentialStatus(device, endpoint_id, credential)) +end + +local function handle_set_code(driver, device, command) + local endpoint = device:component_to_endpoint(command.component) + if (command.args.codePIN == "") then + driver:inject_capability_command( + device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.nameSlot.NAME, + args = {command.args.codeSlot, command.args.codeName}, + } + ) + else + local credential = { + credential_type = DoorLock.types.DlCredentialType.PIN, + credential_index = command.args.codeSlot, + } + device:set_field(lock_utils.SET_CREDENTIAL, command.args.codeSlot) + device:send( + DoorLock.server.commands.SetCredential( + device, endpoint, DoorLock.types.DlDataOperationType.ADD, -- operation_type + credential, command.args.codePIN, -- credential_data + nil, -- nil user_index creates a new user + DoorLock.types.DlUserStatus.OCCUPIED_ENABLED, DoorLock.types.DlUserType.UNRESTRICTED_USER + ) + ) + if (command.args.codeName ~= nil) then + -- wait for confirmation from the lock to commit this to memory + -- Groovy driver has a lot more info passed here as a description string, may need to be investigated + local codeState = device:get_field(lock_utils.CODE_STATE) or {} + codeState["setName" .. command.args.codeSlot] = command.args.codeName + device:set_field(lock_utils.CODE_STATE, codeState, {persist = true}) + end + end +end + +local function handle_name_slot(driver, device, command) + local code_slot = tostring(command.args.codeSlot) + local lock_codes = lock_utils.get_lock_codes(device) + if (lock_codes[code_slot] ~= nil) then + lock_codes[code_slot] = command.args.codeName + device:emit_event( + capabilities.lockCodes.codeChanged( + code_slot .. " renamed", {state_change = true} + ) + ) + lock_utils.lock_codes_event(device, lock_codes) + end +end + +local function json_to_table(input) + if #input == 0 then return end + local result = {} + for key, value in string.gmatch(input, '"(.-)"%s*:%s*"(.-)"') do + table.insert(result, {key, value}) + end + return result +end + +local function store_lock_codes(device) + local lockCodes = device:get_latest_state( + "main", + capabilities.lockCodes.ID, + capabilities.lockCodes.lockCodes.NAME, + {} + ) + lockCodes = json_to_table(lockCodes) + device:set_field(lock_utils.LOCK_CODES_FOR_MIGRATION, lockCodes, {persist = true}) +end + +local function handle_migrate(driver, device, command) + store_lock_codes(device) + device:set_field(lock_utils.LOCK_CODES_COPY_REQUIRED, true, {persist = true}) + device:emit_event(capabilities.lockCodes.migrated(true)) + driver:inject_capability_command(device, { + capability = capabilities.refresh.ID, + command = capabilities.refresh.commands.refresh.NAME, + args = {} + }) +end + +local function find_default_endpoint(device, cluster) + local res = device.MATTER_DEFAULT_ENDPOINT + local eps = device:get_endpoints(cluster) + table.sort(eps) + for _, v in ipairs(eps) do + if v ~= 0 then --0 is the matter RootNode endpoint + return v + end + end + device.log.warn(string.format("Did not find default endpoint, will use endpoint %d instead", device.MATTER_DEFAULT_ENDPOINT)) + return res +end + +local function component_to_endpoint(device, component_name) + return find_default_endpoint(device, clusters.DoorLock.ID) +end + +local function info_changed(driver, device, event, args) + if device.profile.id ~= args.old_st_store.profile.id then + device:subscribe() + end +end + +local function do_configure(driver, device) + -- check if the device is NOT currently profiled as base-lock + -- by ANDing a query for every capability in the base-lock profiles. + -- If it does not use base-lock, it is WWST and does not need re-profiling. + if not (device:supports_capability(capabilities.lock) and + device:supports_capability(capabilities.lockCodes) and + device:supports_capability(capabilities.tamperAlert) and + device:supports_capability(capabilities.battery)) then + return + end + + -- if not fingerprinted, dynamically configure base-lock profile + local profile_name = "base-lock" + device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps == 0 then + profile_name = profile_name .. "-nobattery" + device.log.info(string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + else + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + req:merge(clusters.PowerSource.attributes.AttributeList:read()) + device:send(req) + end +end + +local function device_init(driver, device) + device:set_component_to_endpoint_fn(component_to_endpoint) + device:subscribe() + + -- check if we have a missing COTA credential. Only run this if it has not been run before (i.e. in device added), + -- because there is a delay built into the COTA process and we do not want to start two COTA generations at the same time + -- in the event this was triggered on add. + if not device:get_field(lock_utils.COTA_READ_INITIALIZED) or not device:get_field(lock_utils.COTA_CRED) then + local eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.DoorLockFeature.CREDENTIALSOTA | DoorLock.types.DoorLockFeature.PIN_CREDENTIALS}) + if #eps == 0 then + device.log.debug("Device will not require PIN for remote operation") + device:set_field(lock_utils.COTA_CRED, false, {persist = true}) + else + device:send(DoorLock.attributes.RequirePINforRemoteOperation:read(device, eps[1])) + device:set_field(lock_utils.COTA_READ_INITIALIZED, true, {persist = true}) + end + end +end + +local function device_added(driver, device) + --Note: May want to write OperatingMode to NORMAL, to attempt to ensure remote operation works + --Note: May want to write RequirePINForRemoteOperation, to avoid cota cases if possible. + device:emit_event(capabilities.tamperAlert.tamper.clear()) + local eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.DoorLockFeature.PIN_CREDENTIALS}) + if #eps == 0 then + if device:supports_capability_by_id(capabilities.tamperAlert.ID) then + device.log.debug("Device does not support lockCodes. Switching profile.") + local profile_name = "lock-without-codes" + device:set_field(PROFILE_BASE_NAME, profile_name, {persist = true}) + local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) + if #battery_feature_eps == 0 then + profile_name = profile_name .. "-nobattery" + end + device.log.info(string.format("Updating device profile to %s.", profile_name)) + device:try_update_metadata({profile = profile_name}) + else + device.log.debug("Device supports neither lock codes nor tamper. Unable to switch profile.") + end + else + local req = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) + req:merge(DoorLock.attributes.MaxPINCodeLength:read(device, eps[1])) + req:merge(DoorLock.attributes.MinPINCodeLength:read(device, eps[1])) + req:merge(DoorLock.attributes.NumberOfPINUsersSupported:read(device, eps[1])) + driver:inject_capability_command(device, { + capability = capabilities.lockCodes.ID, + command = capabilities.lockCodes.commands.reloadAllCodes.NAME, + args = {} + }) + + --Device may require pin for remote operation if it supports COTA and PIN features. + eps = device:get_endpoints(DoorLock.ID, {feature_bitmap = DoorLock.types.DoorLockFeature.CREDENTIALSOTA | DoorLock.types.DoorLockFeature.PIN_CREDENTIALS}) + if #eps == 0 then + device.log.debug("Device will not require PIN for remote operation") + device:set_field(lock_utils.COTA_CRED, false, {persist = true}) + else + req:merge(DoorLock.attributes.RequirePINforRemoteOperation:read(device, eps[1])) + device:set_field(lock_utils.COTA_READ_INITIALIZED, true, {persist = true}) + end + device:send(req) + end +end + +local unmigrated_matter_lock_driver = { + NAME = "Unmigrated Matter Lock Driver", + matter_handlers = { + attr = { + [DoorLock.ID] = { + [DoorLock.attributes.LockState.ID] = lock_state_handler, + [DoorLock.attributes.MaxPINCodeLength.ID] = max_pin_code_len_handler, + [DoorLock.attributes.MinPINCodeLength.ID] = min_pin_code_len_handler, + [DoorLock.attributes.NumberOfPINUsersSupported.ID] = num_pin_users_handler, + [DoorLock.attributes.RequirePINforRemoteOperation.ID] = require_remote_pin_handler, + }, + [PowerSource.ID] = { + [PowerSource.attributes.AttributeList.ID] = handle_power_source_attribute_list, + [PowerSource.attributes.BatPercentRemaining.ID] = handle_battery_percent_remaining, + [PowerSource.attributes.BatChargeLevel.ID] = handle_battery_charge_level, + }, + }, + event = { + [DoorLock.ID] = { + [DoorLock.events.DoorLockAlarm.ID] = alarm_event_handler, + [DoorLock.events.LockOperation.ID] = lock_op_event_handler, + [DoorLock.events.LockUserChange.ID] = lock_user_change_event_handler, + }, + }, + cmd_response = { + [DoorLock.ID] = { + [DoorLock.client.commands.SetCredentialResponse.ID] = set_credential_response_handler, + [DoorLock.client.commands.GetCredentialStatusResponse.ID] = get_credential_status_response_handler, + [DoorLock.server.commands.ClearCredential.ID] = clear_credential_response_handler, + }, + }, + }, + subscribed_attributes = subscribed_attributes, + subscribed_events = { + [capabilities.tamperAlert.ID] = {DoorLock.events.DoorLockAlarm, DoorLock.events.LockOperation}, + [capabilities.lockAlarm.ID] = {DoorLock.events.DoorLockAlarm}, + [capabilities.lockCodes.ID] = {DoorLock.events.LockUserChange}, + }, + capability_handlers = { + [capabilities.refresh.ID] = {[capabilities.refresh.commands.refresh.NAME] = handle_refresh}, + [capabilities.lock.ID] = { + [capabilities.lock.commands.lock.NAME] = handle_lock, + [capabilities.lock.commands.unlock.NAME] = handle_unlock, + }, + [capabilities.lockCodes.ID] = { + [capabilities.lockCodes.commands.deleteCode.NAME] = handle_delete_code, + [capabilities.lockCodes.commands.reloadAllCodes.NAME] = handle_reload_all_codes, + [capabilities.lockCodes.commands.requestCode.NAME] = handle_request_code, + [capabilities.lockCodes.commands.setCode.NAME] = handle_set_code, + [capabilities.lockCodes.commands.nameSlot.NAME] = handle_name_slot, + [capabilities.lockCodes.commands.migrate.NAME] = handle_migrate, + }, + }, + supported_capabilities = { + capabilities.lock, + capabilities.lockCodes, + capabilities.tamperAlert, + capabilities.battery, + capabilities.batteryLevel, + }, + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + infoChanged = info_changed, + }, + can_handle = require("unmigrated-matter-lock.can_handle"), +} + +return unmigrated_matter_lock_driver From 8ca1717ec47622ce3d7b09c10f6d27a70724adb4 Mon Sep 17 00:00:00 2001 From: Juho Date: Fri, 10 Jul 2026 10:33:07 +0900 Subject: [PATCH 024/102] zwave-lock: respond to Time DATE_GET with DATE_REPORT The driver only handled TIME_GET, so a lock requesting the date during a Time CC sync never got an answer (the hub ACKs at the MAC layer but no DATE_REPORT is sent). Add a DATE_GET handler that reports the current date, following the reworked lock_handlers structure. Fixes #3076 --- drivers/SmartThings/zwave-lock/src/init.lua | 3 ++- .../src/lock_handlers/zwave_responses.lua | 13 ++++++++++++ .../zwave-lock/src/test/test_lock_battery.lua | 21 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 8b2cf76eb8..6c85639625 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -77,7 +77,8 @@ local driver_template = { }, zwave_handlers = { [cc.TIME] = { - [0x01] = zwave_handlers.time_get_handler -- used by DanaLock + [0x01] = zwave_handlers.time_get_handler, -- used by DanaLock + [0x03] = zwave_handlers.date_get_handler }, [cc.NOTIFICATION] = { [Notification.REPORT] = zwave_handlers.notification_report diff --git a/drivers/SmartThings/zwave-lock/src/lock_handlers/zwave_responses.lua b/drivers/SmartThings/zwave-lock/src/lock_handlers/zwave_responses.lua index cfabe41932..7822ab2bfe 100644 --- a/drivers/SmartThings/zwave-lock/src/lock_handlers/zwave_responses.lua +++ b/drivers/SmartThings/zwave-lock/src/lock_handlers/zwave_responses.lua @@ -240,4 +240,17 @@ function ZwaveHandlers.time_get_handler(driver, device, cmd) ) end +function ZwaveHandlers.date_get_handler(driver, device, cmd) + local Time = (require "st.zwave.CommandClass.Time")({ version = 1 }) + local time = os.date("*t") + device:send_to_component( + Time:DateReport({ + year = time.year, + month = time.month, + day = time.day + }), + device:endpoint_to_component(cmd.src_channel) + ) +end + return ZwaveHandlers diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua index 11c03650c6..d9642851e1 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -140,4 +140,25 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "The driver should respond correctly to a date get", + function () + test.socket.zwave:__queue_receive({ mock_device.id, Time:DateGet({},{ + encap = zw.ENCAP.AUTO, + src_channel = 0, + dst_channels = {} + }) + }) + local time = os.date("*t") + test.socket.zwave:__expect_send(Time:DateReport({ + year = time.year, + month = time.month, + day = time.day + }):build_test_tx(mock_device.id)) + end, + { + min_api_version = 17 + } +) + test.run_registered_tests() From c4126ccdede6d67275efb309f86fce355af1862f 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 025/102] 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 95280338c049a9ef14b832c3af9da6e1549b517d Mon Sep 17 00:00:00 2001 From: nickolas-deboom <158304111+nickolas-deboom@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:41:02 -0500 Subject: [PATCH 026/102] Hue: fix issue with small color temp increments (#2771) Incrementing or decrementing the color temp by 1 can lead to the driver sending the Hue bridge the same value in mireds as the device is already set to, and the bridge not responding, which in turn leads to a spinning wheel in the app and eventually the value being reset to its previous value. This change emits the capability with the updated value without waiting for a response from the bridge. --------- Co-authored-by: Harrison Carter --- .../src/handlers/attribute_emitters.lua | 13 +++++++++ .../philips-hue/src/handlers/commands.lua | 2 ++ .../src/handlers/grouped_light_commands.lua | 27 +++++++++++-------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua index f274ef37f0..05c9dbb7e9 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/attribute_emitters.lua @@ -158,6 +158,19 @@ local function _emit_light_events_inner(light_device, light_repr) end end +-- A sufficiently small color temp command can transform to the same value (in mireks) that a device already has, so the bridge +-- may not respond to said command. To avoid any odd behaviors in this case, we should emit a kelvin value change event immediately +function AttributeEmitters.emit_color_temp_when_mirek_unchanged(device, clamped_kelvin, mirek) + local current_color_temp = device:get_latest_state("main", capabilities.colorTemperature.ID, capabilities.colorTemperature.colorTemperature.NAME) + if current_color_temp then + local current_mirek = math.floor(utils.kelvin_to_mirek(current_color_temp)) + if current_mirek == mirek then + log.debug(string.format("Color temp change from %dK to %dK results in same mirek value (%d), emitting event directly", current_color_temp, clamped_kelvin, mirek)) + device:emit_event(capabilities.colorTemperature.colorTemperature(clamped_kelvin)) + end + end +end + function AttributeEmitters.connectivity_update(child_device, zigbee_status) if child_device == nil or (child_device and child_device.id == nil) then log.warn("Tried to emit attribute events for a device that has been deleted") diff --git a/drivers/SmartThings/philips-hue/src/handlers/commands.lua b/drivers/SmartThings/philips-hue/src/handlers/commands.lua index 2c55f39452..480f003769 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/commands.lua @@ -7,6 +7,7 @@ st_utils.stringify_table = st_utils.stringify_table local Consts = require "consts" local Fields = require "fields" local HueColorUtils = require "utils.cie_utils" +local attribute_emitters = require "handlers.attribute_emitters" local utils = require "utils" @@ -154,6 +155,7 @@ local function do_color_temp_action(driver, device, args) local min = device:get_field(Fields.MIN_KELVIN) or Consts.MIN_TEMP_KELVIN_WHITE_AMBIANCE local clamped_kelvin = st_utils.clamp_value(kelvin, min, Consts.MAX_TEMP_KELVIN) local mirek = math.floor(utils.kelvin_to_mirek(clamped_kelvin)) + attribute_emitters.emit_color_temp_when_mirek_unchanged(device, clamped_kelvin, mirek) local resp, err = hue_api:set_light_color_temp(light_id, mirek) log_command_response_errors(resp, err, "color temp action") diff --git a/drivers/SmartThings/philips-hue/src/handlers/grouped_light_commands.lua b/drivers/SmartThings/philips-hue/src/handlers/grouped_light_commands.lua index 1a1099fc5e..266971b99c 100644 --- a/drivers/SmartThings/philips-hue/src/handlers/grouped_light_commands.lua +++ b/drivers/SmartThings/philips-hue/src/handlers/grouped_light_commands.lua @@ -4,6 +4,7 @@ local Consts = require "consts" local Fields = require "fields" local HueColorUtils = require "utils.cie_utils" local grouped_utils = require "utils.grouped_utils" +local attribute_emitters = require "handlers.attribute_emitters" local utils = require "utils" @@ -94,7 +95,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common local function do_color_action(driver, bridge_device, group, args, aux) local hue, sat = (args.args.color.hue / 100), (args.args.color.saturation / 100) if hue == 1 then -- 0 and 360 degrees are equivalent in HSV, but not in our conversion function @@ -135,7 +136,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common local function do_setHue_action(driver, bridge_device, group, args, aux) local currentSaturation = aux[Fields.COLOR_SATURATION] or 0 args.args.color = { @@ -149,7 +150,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common local function do_setSaturation_action(driver, bridge_device, group, args, aux) local currentHue = aux[Fields.COLOR_HUE] or 0 args.args.color = { @@ -163,7 +164,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common local function do_color_temp_action(driver, bridge_device, group, args, aux) local kelvin = args.args.temperature @@ -185,6 +186,10 @@ local function do_color_temp_action(driver, bridge_device, group, args, aux) local clamped_kelvin = st_utils.clamp_value(kelvin, min, Consts.MAX_TEMP_KELVIN) local mirek = math.floor(utils.kelvin_to_mirek(clamped_kelvin)) + for _, device in ipairs(group.devices) do + attribute_emitters.emit_color_temp_when_mirek_unchanged(device, clamped_kelvin, mirek) + end + local resp, err = hue_api:set_grouped_light_color_temp(grouped_light_id, mirek) if not resp or (resp.errors and #resp.errors == 0) then @@ -203,7 +208,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common function GroupedLightCommandHandlers.switch_on_handler(driver, bridge_device, group, args, aux) do_switch_action(driver, bridge_device, group, args) end @@ -212,7 +217,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common function GroupedLightCommandHandlers.switch_off_handler(driver, bridge_device, group, args, aux) do_switch_action(driver, bridge_device, group, args) end @@ -221,7 +226,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common function GroupedLightCommandHandlers.switch_level_handler(driver, bridge_device, group, args, aux) do_switch_level_action(driver, bridge_device, group, args) end @@ -230,7 +235,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common function GroupedLightCommandHandlers.set_color_handler(driver, bridge_device, group, args, aux) do_color_action(driver, bridge_device, group, args, aux) end @@ -239,7 +244,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common function GroupedLightCommandHandlers.set_hue_handler(driver, bridge_device, group, args, aux) do_setHue_action(driver, bridge_device, group, args, aux) end @@ -248,7 +253,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common function GroupedLightCommandHandlers.set_saturation_handler(driver, bridge_device, group, args, aux) do_setSaturation_action(driver, bridge_device, group, args, aux) end @@ -257,7 +262,7 @@ end ---@param bridge_device HueBridgeDevice ---@param group table ---@param args table ----@param aux table auxilary data needed for the command that the devices all had in common +---@param aux table auxiliary data needed for the command that the devices all had in common function GroupedLightCommandHandlers.set_color_temp_handler(driver, bridge_device, group, args, aux) do_color_temp_action(driver, bridge_device, group, args, aux) end From cd56d787c8876971c44a73e9dfbc77eae789a875 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 13 Jul 2026 10:19:20 -0500 Subject: [PATCH 027/102] Matter Ikea Scroll: Add rotation sensitivity preferences (#3100) --- .../matter-switch/profiles/ikea-scroll.yml | 9 +- .../scroll_handlers/event_handlers.lua | 6 +- .../ikea_scroll/scroll_utils/event_utils.lua | 14 ++ .../ikea_scroll/scroll_utils/fields.lua | 7 + .../src/test/test_ikea_scroll.lua | 196 ++++++++++++++++++ 5 files changed, 230 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml index 973c789ac6..1793ae209e 100644 --- a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml +++ b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml @@ -33,6 +33,13 @@ components: version: 1 categories: - name: Button +preferences: + - preferenceId: knobSensitivityGroup1 + explicit: true + - preferenceId: knobSensitivityGroup2 + explicit: true + - preferenceId: knobSensitivityGroup3 + explicit: true metadata: mnmn: SmartThingsEdge - vid: ikea-scroll \ No newline at end of file + vid: ikea-scroll diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua index dbad323784..1fac107a75 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_handlers/event_handlers.lua @@ -10,12 +10,16 @@ local event_utils = require "sub_drivers.ikea_scroll.scroll_utils.event_utils" local IkeaScrollEventHandlers = {} + local function rotate_amount_event_helper(device, endpoint_id, num_presses_to_handle) if num_presses_to_handle <= 0 then return end -- to cut down on checks, we can assume that if the endpoint is not in ENDPOINTS_UP_SCROLL, it is in ENDPOINTS_DOWN_SCROLL local scroll_direction = switch_utils.tbl_contains(scroll_fields.ENDPOINTS_UP_SCROLL, endpoint_id) and 1 or -1 - local scroll_amount = st_utils.clamp_value(scroll_direction * scroll_fields.PER_SCROLL_EVENT_ROTATION * num_presses_to_handle, -100, 100) + local scroll_amount = st_utils.clamp_value( + event_utils.rotate_amount_scaling_helper(device, endpoint_id, scroll_direction * scroll_fields.PER_SCROLL_EVENT_ROTATION * num_presses_to_handle), + -100, 100 + ) if event_utils.is_valid_scroll_amount(device, scroll_amount) then device:emit_event_for_endpoint(endpoint_id, capabilities.knob.rotateAmount(scroll_amount, {state_change = true})) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/event_utils.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/event_utils.lua index 8a7a684cf6..d5568d2c9f 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/event_utils.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/event_utils.lua @@ -73,4 +73,18 @@ function IkeaScrollEventUtils.aggregate_scroll_amount_for_info_blocks(device, in return { total_presses = total_presses, presses_in_current_chain = presses_in_current_chain } end +function IkeaScrollEventUtils.rotate_amount_scaling_helper(device, endpoint_id, scroll_amount) + local scale_factor = 1.0 -- the default scale factor is 1.0, i.e none + local component = device:endpoint_to_component(endpoint_id) + if component == "main" then + scale_factor = scroll_fields.KNOB_SENSITIVITY_PREFERENCE_FACTORS[device.preferences.knobSensitivityGroup1] or 1.0 + elseif component == "group2" then + scale_factor = scroll_fields.KNOB_SENSITIVITY_PREFERENCE_FACTORS[device.preferences.knobSensitivityGroup2] or 1.0 + elseif component == "group3" then + scale_factor = scroll_fields.KNOB_SENSITIVITY_PREFERENCE_FACTORS[device.preferences.knobSensitivityGroup3] or 1.0 + end + local scaled_scroll_amount = st_utils.clamp_value(math.floor(scroll_amount * scale_factor), -100, 100) + return scaled_scroll_amount +end + return IkeaScrollEventUtils diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua index 27e0dbcd6d..2445e2fd30 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/ikea_scroll/scroll_utils/fields.lua @@ -33,6 +33,13 @@ IkeaScrollFields.CLEAR_STATE_TIMER = "__clear_state_timer" -- Delay in seconds to wait before clearing the global rotate amount state after the last scroll event IkeaScrollFields.CLEAR_STATE_DELAY_S = 8 +-- Amounts by which to multiply the scroll amount based on user preference for sensitivity +IkeaScrollFields.KNOB_SENSITIVITY_PREFERENCE_FACTORS = { + ["0"] = 0.5, -- Low + ["1"] = 1.0, -- Medium + ["2"] = 2.0 -- High +} + -- Required Events for the ENDPOINTS_PUSH. IkeaScrollFields.switch_press_subscribed_events = { clusters.Switch.events.InitialPress.ID, diff --git a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua index f421b17f79..be28bd040a 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -749,4 +749,200 @@ test.register_coroutine_test( end ) +-- Coroutine tests for sensitivity preference logic. +-- KNOB_SENSITIVITY_FACTORS = {0.5, 1.0, 2.0}, indexed by the preference value. +-- nil preference falls back to 1.0 (tested by the message tests above). + +test.register_coroutine_test( + "Ikea Scroll low sensitivity (factor 0.5) scales rotateAmount correctly on main", + function() + test.socket.device_lifecycle():__queue_receive(mock_ikea_scroll:generate_info_changed({ preferences = { knobSensitivityGroup1 = "0" } })) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 1} + ) + }) + + -- MultiPressOngoing count=2 (delta=2) → floor(12 * 0.5) = 6 + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {current_number_of_presses_counted = 2, new_position = 2} + ) + }) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(6, {state_change = true}))) + + -- MultiPressOngoing count=5 (delta=3) → floor(18 * 0.5) = 9 + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {current_number_of_presses_counted = 5, new_position = 5} + ) + }) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(9, {state_change = true}))) + + -- MultiPressComplete total=5 (delta=0, no event emitted) + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }) + + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Ikea Scroll high sensitivity (factor 2.0) scales rotateAmount correctly on main", + function() + test.socket.device_lifecycle():__queue_receive(mock_ikea_scroll:generate_info_changed({ preferences = { knobSensitivityGroup1 = "2" } })) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 1} + ) + }) + + -- MultiPressOngoing count=2 (delta=2) → floor(12 * 2.0) = 24 + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {current_number_of_presses_counted = 2, new_position = 2} + ) + }) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(24, {state_change = true}))) + + -- MultiPressOngoing count=5 (delta=3) → floor(18 * 2.0) = 36 + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {current_number_of_presses_counted = 5, new_position = 5} + ) + }) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("main", + capabilities.knob.rotateAmount(36, {state_change = true}))) + + -- MultiPressComplete total=5 (delta=0, no event emitted) + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[1], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }) + + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Ikea Scroll high sensitivity (factor 2.0) scales rotateAmount correctly on group2", + function() + test.socket.device_lifecycle():__queue_receive(mock_ikea_scroll:generate_info_changed({ preferences = { knobSensitivityGroup2 = "2" } })) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[3], {new_position = 1} + ) + }) + + -- MultiPressOngoing count=2 (delta=2) → floor(12 * 2.0) = 24 + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[3], {current_number_of_presses_counted = 2, new_position = 2} + ) + }) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(24, {state_change = true}))) + + -- MultiPressOngoing count=5 (delta=3) → floor(18 * 2.0) = 36 + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[3], {current_number_of_presses_counted = 5, new_position = 5} + ) + }) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group2", + capabilities.knob.rotateAmount(36, {state_change = true}))) + + -- MultiPressComplete total=5 (delta=0, no event emitted) + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[3], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }) + + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Ikea Scroll low sensitivity (factor 0.5) scales rotateAmount correctly on group3", + function() + test.socket.device_lifecycle():__queue_receive(mock_ikea_scroll:generate_info_changed({ preferences = { knobSensitivityGroup3 = "0" } })) + test.wait_for_events() + + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.InitialPress:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[5], {new_position = 1} + ) + }) + + -- MultiPressOngoing count=2 (delta=2) → floor(12 * 0.5) = 6 + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[5], {current_number_of_presses_counted = 2, new_position = 2} + ) + }) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(6, {state_change = true}))) + + -- MultiPressOngoing count=5 (delta=3) → floor(18 * 0.5) = 9 + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressOngoing:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[5], {current_number_of_presses_counted = 5, new_position = 5} + ) + }) + test.socket.capability:__expect_send(mock_ikea_scroll:generate_test_message("group3", + capabilities.knob.rotateAmount(9, {state_change = true}))) + + -- MultiPressComplete total=5 (delta=0, no event emitted) + test.socket.matter:__queue_receive({ + mock_ikea_scroll.id, + clusters.Switch.events.MultiPressComplete:build_test_event_report( + mock_ikea_scroll, ENDPOINTS_SCROLL[5], {new_position = 5, total_number_of_presses_counted = 5, previous_position = 0} + ) + }) + + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + test.run_registered_tests() From db11e084578211ee7321295be280947ae3ca6043 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 028/102] 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 9a0c5866129a766d234adeed537b7ecbb061bcad Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Tue, 14 Jul 2026 11:53:21 -0500 Subject: [PATCH 029/102] WWSTCERT-12300 Aqara Smart Lock U500 (#3036) * WWSTCERT-12300 Aqara Smart Lock U500 * Update Aqara lock fingerprint descriptions * Fix U500 fingerprint profile * Remove obsolete matter-lock fingerprints file * chore: retrigger PR checks --- drivers/SmartThings/matter-lock/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 6d4d068b71..bfa19905a5 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -25,6 +25,11 @@ matterManufacturer: vendorId: 0x115F productId: 0x2804 deviceProfileName: lock + - id: "4447/10254" + deviceLabel: Aqara Smart Lock U500 + vendorId: 0x115F + productId: 0x280E + deviceProfileName: lock-modular - id: "4447/10255" deviceLabel: Aqara Smart Lock U500 Rim vendorId: 0x115F From 7853d5452036db05d4cde9aaa97de4d5d6b93dfd 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 030/102] 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 ba49f38b8900912836b7bddc4cbdb089b0882150 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 031/102] 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 3ce648328656c844b4df88bbc7a505c1480087a7 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 032/102] 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 d550b8fda7ca225c41ce3057211c474e25661946 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 033/102] 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 7c292203c434ea1bff638aa7c26447dec628143e 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 034/102] 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 ac1f0b7425927d9092864ed44ef735620f3c2ebc 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 035/102] 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 6e2441021cc28d32e2a6aa817ebd78be08cf5319 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 036/102] 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 fd65fe9e4145fcfd6e330702cf9f10722febf67b 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 037/102] 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 5da711f17cc8dccf3d591b906de89eba3cc236b5 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 038/102] 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 e44f98d32e6e16fa8da356e6aced71f428d5c3bd 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 039/102] 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 91803c1aeaeb56c97a2150414c786ff197ef1eb1 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 040/102] 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 cc30a5eb5fb113f8d2844ee23eb15d0e0f1060dc 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 041/102] 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 bc3100649699fa49fb22de48cbba563d293715cc 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 042/102] 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 3fdc1cbbd19b115d84601b773d349b33edc640a1 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 043/102] 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 65ea2a8e9bc06fe8751716780a41829c2a02c92d 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 044/102] 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 664cef00e26baaf6fc2d1b87c7f8d0164bed7159 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 045/102] 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 2f1dcd9fb644e1b2088fd81d9040666a4d06a0c2 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 046/102] 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 7acd4d7ea9f7bcb36ce42efb91b762451f7363b5 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 047/102] 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 d912324f90c10d85530d415f9878eff5d19a8c4f Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 15 Jul 2026 10:55:14 -0500 Subject: [PATCH 048/102] remove knob preferences for ikea scroll --- drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml index 1793ae209e..2f4f53542f 100644 --- a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml +++ b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml @@ -33,13 +33,6 @@ components: version: 1 categories: - name: Button -preferences: - - preferenceId: knobSensitivityGroup1 - explicit: true - - preferenceId: knobSensitivityGroup2 - explicit: true - - preferenceId: knobSensitivityGroup3 - explicit: true metadata: mnmn: SmartThingsEdge vid: ikea-scroll From 3e0a850adc03d2513bc5a3c2ee3612975a83617e Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Wed, 15 Jul 2026 10:57:00 -0500 Subject: [PATCH 049/102] re-add knob preferences for ikea scroll --- drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml index 2f4f53542f..1793ae209e 100644 --- a/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml +++ b/drivers/SmartThings/matter-switch/profiles/ikea-scroll.yml @@ -33,6 +33,13 @@ components: version: 1 categories: - name: Button +preferences: + - preferenceId: knobSensitivityGroup1 + explicit: true + - preferenceId: knobSensitivityGroup2 + explicit: true + - preferenceId: knobSensitivityGroup3 + explicit: true metadata: mnmn: SmartThingsEdge vid: ikea-scroll From 0255dd3614f8b9282a053b4cd9a7cf3b4e557d4e Mon Sep 17 00:00:00 2001 From: Cooper Towns Date: Thu, 16 Jul 2026 10:00:14 -0500 Subject: [PATCH 050/102] Matter Sensor: Fix Global defintion and adjust unit tests --- .../Global/types/MeasurementTypeEnum.lua | 2 +- .../src/sensor_handlers/attribute_handlers.lua | 14 +++++++++++--- .../src/test/test_matter_soil_sensor.lua | 10 +++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua index 2e749ebacd..334ad78331 100644 --- a/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua +++ b/drivers/SmartThings/matter-sensor/src/embedded_clusters/Global/types/MeasurementTypeEnum.lua @@ -2,7 +2,7 @@ local data_types = require "st.matter.data_types" local UintABC = require "st.matter.data_types.base_defs.UintABC" local MeasurementTypeEnum = {} -local new_mt = UintABC.new_mt({NAME = "MeasurementTypeEnum", ID = data_types.name_to_id_map["Uint8"]}, 1) +local new_mt = UintABC.new_mt({NAME = "MeasurementTypeEnum", ID = data_types.name_to_id_map["Uint16"]}, 2) new_mt.__index.pretty_print = function(self) local name_lookup = { [self.UNSPECIFIED] = "UNSPECIFIED", diff --git a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua index eaa8730fc4..1867d65e0b 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua @@ -13,6 +13,12 @@ if version.api < 13 then clusters.Global = require "embedded_clusters.Global" end +-- The SOIL_MOISTURE MeasurementTypeEnum variant was added to the Global MeasurementTypeEnum +-- def in lua libs in api version 21 as a part of the fix for Shared and Global types. +if version.api < 21 then + clusters.Global.types.MeasurementTypeEnum = require "embedded_clusters.Global.types.MeasurementTypeEnum" +end + local AttributeHandlers = {} @@ -84,8 +90,10 @@ function AttributeHandlers.soil_moisture_measured_value_handler(driver, device, end function AttributeHandlers.soil_moisture_measurement_limits_handler(driver, device, ib, response) - local MeasurementAccuracyStruct = require "embedded_clusters.Global.types.MeasurementAccuracyStruct" - MeasurementAccuracyStruct:augment_type(ib.data) + if version.api < 13 then + local MeasurementAccuracyStruct = require "embedded_clusters.Global.types.MeasurementAccuracyStruct" + MeasurementAccuracyStruct:augment_type(ib.data) + end local min_val = ib.data.elements and ib.data.elements.min_measured_value and ib.data.elements.min_measured_value.value local max_val = ib.data.elements and ib.data.elements.max_measured_value and ib.data.elements.max_measured_value.value if not (min_val and max_val) or (min_val >= max_val) or (min_val < sensor_utils.SOIL_MOISTURE_MIN) or (max_val > sensor_utils.SOIL_MOISTURE_MAX) then @@ -229,4 +237,4 @@ function AttributeHandlers.flow_measured_value_bounds_factory(minOrMax) end end -return AttributeHandlers \ No newline at end of file +return AttributeHandlers diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua index 0c845fa47e..a5a8042fc0 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua @@ -7,10 +7,15 @@ local t_utils = require "integration_test.utils" local test = require "integration_test" local version = require "version" -clusters.Global = require "embedded_clusters.Global" +if version.api < 13 then + clusters.Global = require "embedded_clusters.Global" +end if version.api < 21 then clusters.SoilMeasurement = require "embedded_clusters.SoilMeasurement" + -- The SOIL_MOISTURE MeasurementTypeEnum variant was added to the Global MeasurementTypeEnum + -- def in lua libs in api version 21. + clusters.Global.types.MeasurementTypeEnum = require "embedded_clusters.Global.types.MeasurementTypeEnum" end local mock_device = test.mock_device.build_test_matter_device({ @@ -157,6 +162,9 @@ test.register_coroutine_test( ) local function build_soil_moisture_limits(min_value, max_value) + if version.api < 21 then + clusters.Global.types.MeasurementTypeEnum = require "embedded_clusters.Global.types.MeasurementTypeEnum" + end return clusters.Global.types.MeasurementAccuracyStruct({ measurement_type = clusters.Global.types.MeasurementTypeEnum.SOIL_MOISTURE, measured = true, From 1be5fd4968509e4738e9fb22428437295872cb25 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 8 Jul 2026 17:11:17 -0500 Subject: [PATCH 051/102] Aligned handlers and fields in value magnitude and added tests --- .../src/test/test_zenwithin_thermostat.lua | 361 ++++++++++++++++++ .../zigbee-thermostat/src/zenwithin/init.lua | 6 +- 2 files changed, 364 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua index 947923e33f..721853579a 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua @@ -523,4 +523,365 @@ test.register_coroutine_test( } ) +test.register_message_test( + "MinHeatSetpointLimit report stores device field", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.MinHeatSetpointLimit:build_test_attr_report(mock_device, 500) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "MaxHeatSetpointLimit report stores device field", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.MaxHeatSetpointLimit:build_test_attr_report(mock_device, 3500) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "MinCoolSetpointLimit report stores device field", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.MinCoolSetpointLimit:build_test_attr_report(mock_device, 1600) } + } + }, + { + min_api_version = 17 + } +) + +test.register_message_test( + "MaxCoolSetpointLimit report stores device field", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Thermostat.attributes.MaxCoolSetpointLimit:build_test_attr_report(mock_device, 3200) } + } + }, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Heating setpoint is clamped to MinHeatSetpointLimit", + function() + -- Receive MinHeatSetpointLimit = 18.0 C (1800 centidegrees) + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MinHeatSetpointLimit:build_test_attr_report(mock_device, 1800) + }) + test.wait_for_events() + + -- Put device in heat mode so the setpoint is applied immediately + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, 0x04) -- HEAT + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.heat()) + ) + test.wait_for_events() + + -- Try to set heating setpoint below the stored min (16 C < 18 C); expect it clamped to 18 C + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 16 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 1800) -- clamped to 18.0 C + }) + test.mock_time.advance_time(2) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Heating setpoint is clamped to MaxHeatSetpointLimit", + function() + -- Receive MaxHeatSetpointLimit = 30.0 C (3000 centidegrees) + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MaxHeatSetpointLimit:build_test_attr_report(mock_device, 3000) + }) + test.wait_for_events() + + -- Put device in heat mode + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, 0x04) -- HEAT + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.heat()) + ) + test.wait_for_events() + + -- Try to set heating setpoint above the stored max (35 C > 30 C); expect it clamped to 30 C + test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatHeatingSetpoint", component = "main", command = "setHeatingSetpoint", args = { 35 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 3000) -- clamped to 30.0 C + }) + test.mock_time.advance_time(2) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Cooling setpoint is clamped to MinCoolSetpointLimit", + function() + -- Receive MinCoolSetpointLimit = 18.0 C (1800 centidegrees) + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MinCoolSetpointLimit:build_test_attr_report(mock_device, 1800) + }) + test.wait_for_events() + + -- Put device in cool mode so the setpoint is applied immediately + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, 0x03) -- COOL + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.cool()) + ) + test.wait_for_events() + + -- Try to set cooling setpoint below the stored min (16 C < 18 C); expect it clamped to 18 C + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 16 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 1800) -- clamped to 18.0 C + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Cooling setpoint is clamped to MaxCoolSetpointLimit", + function() + -- Receive MaxCoolSetpointLimit = 30.0 C (3000 centidegrees) + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MaxCoolSetpointLimit:build_test_attr_report(mock_device, 3000) + }) + test.wait_for_events() + + -- Put device in cool mode + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, 0x03) -- COOL + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.cool()) + ) + test.wait_for_events() + + -- Try to set cooling setpoint above the stored max (35 C > 30 C); expect it clamped to 30 C + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 35 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 3000) -- clamped to 30.0 C + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +-- Integration test: full set_cooling_setpoint clamping path +-- +-- Exercises every branch in set_cooling_setpoint that involves get_field / default: +-- +-- Phase 1 – no limit fields present yet → clamp uses DEFAULT_MIN (4.0) / DEFAULT_MAX (37.5) +-- A value already inside the defaults passes through unchanged. +-- A value above DEFAULT_MAX is clamped down to 37.5 C. +-- +-- Phase 2 – device reports MinCoolSetpointLimit (20.0 C) and MaxCoolSetpointLimit (28.0 C) +-- stored via setpoint_limit_handler → get_field now returns real device limits. +-- A value inside those limits passes through unchanged. +-- A value below MinCoolSetpointLimit is clamped up to 20.0 C. +-- A value above MaxCoolSetpointLimit is clamped down to 28.0 C. +-- +-- Phase 3 – device switches to heat mode. +-- setCoolingSetpoint is rejected: driver defers with 10s backstop, +-- then re-emits the last known cooling setpoint without writing to the device. +test.register_coroutine_test( + "set_cooling_setpoint: default clamp, then device-reported limit clamp, then heat-mode rejection", + function() + -- ── Phase 1: no limit fields – defaults apply ───────────────────────────── + + -- Put device in cool mode so setpoints are applied immediately throughout phase 1 & 2 + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, 0x03) -- COOL + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.cool()) + ) + test.wait_for_events() + + -- 1a. Value within defaults (25 C): written as-is + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 25 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 2500) + }) + test.wait_for_events() + + -- Establish the device's reported current cooling setpoint state (needed later for re-emit) + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:build_test_attr_report(mock_device, 2500) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 25.0, unit = "C" })) + ) + test.wait_for_events() + + -- 1b. Value above DEFAULT_MAX (38 C > 37.5 C): clamped to 37.5 C → written as 3750 + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 38 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 3750) + }) + test.wait_for_events() + + -- ── Phase 2: device reports real limits; get_field overrides defaults ────── + + -- Device sends MinCoolSetpointLimit = 20.0 C and MaxCoolSetpointLimit = 28.0 C + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MinCoolSetpointLimit:build_test_attr_report(mock_device, 2000) + }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MaxCoolSetpointLimit:build_test_attr_report(mock_device, 2800) + }) + test.wait_for_events() + + -- 2a. Value inside device limits (24 C): written as-is + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 24 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 2400) + }) + test.wait_for_events() + + -- 2b. Value below device MinCoolSetpointLimit (18 C < 20 C): clamped to 20.0 C + -- (18 is above DEFAULT_MIN of 4.0, proving the device field is used, not the default) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 18 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 2000) + }) + test.wait_for_events() + + -- 2c. Value above device MaxCoolSetpointLimit (30 C > 28 C): clamped to 28.0 C + -- (30 is below DEFAULT_MAX of 37.5, proving the device field is used, not the default) + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 30 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 2800) + }) + test.wait_for_events() + + -- ── Phase 3: device switches to heat mode; setCoolingSetpoint is deferred ── + + -- Re-establish a known current cooling setpoint for the re-emit assertion + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.OccupiedCoolingSetpoint:build_test_attr_report(mock_device, 2400) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 24.0, unit = "C" })) + ) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.SystemMode:build_test_attr_report(mock_device, 0x04) -- HEAT + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.heat()) + ) + test.wait_for_events() + + -- 3. setCoolingSetpoint while in heat mode: driver stores the field (clamped to 24 C by + -- device limits), then takes the else-branch and arms the 10s backstop timer. + -- After the timer fires, update_device_setpoint detects heat mode, discards the pending + -- cooling value, and re-emits the last reported cooling setpoint (24.0 C) unchanged. + test.timer.__create_and_queue_test_time_advance_timer(10, "oneshot") + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "thermostatCoolingSetpoint", component = "main", command = "setCoolingSetpoint", args = { 24 } } + }) + test.wait_for_events() + + test.mock_time.advance_time(10) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({ value = 24.0, unit = "C" })) + ) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua index 1b71db2609..f614eb3210 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua @@ -70,7 +70,7 @@ end local setpoint_limit_handler = function(limit_type) return function(driver, device, limit) - device:set_field(limit_type, limit.value, {persist = true}) + device:set_field(limit_type, limit.value / 100.0, {persist = true}) end end @@ -122,8 +122,8 @@ local set_cooling_setpoint = function(driver, device, command) value = utils.f_to_c(value) end value = utils.clamp_value(value, - device:get_field(MIN_HEAT_LIMIT) or DEFAULT_MIN_SETPOINT, - device:get_field(MAX_HEAT_LIMIT) or DEFAULT_MAX_SETPOINT) + device:get_field(MIN_COOL_LIMIT) or DEFAULT_MIN_SETPOINT, + device:get_field(MAX_COOL_LIMIT) or DEFAULT_MAX_SETPOINT) device:set_field(COOLING_SETPOINT, value) local current_mode = device:get_latest_state("main", ThermostatMode.ID, ModeAttribute.NAME) if current_mode == ModeAttribute.cool.NAME or current_mode == ModeAttribute.auto.NAME then From d8fc96762014e968235f6eab03e8e59308b64418 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Thu, 9 Jul 2026 15:20:29 -0500 Subject: [PATCH 052/102] Scaled zenwithin subdriver to centidegrees --- .../zigbee-thermostat/src/zenwithin/init.lua | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua index f614eb3210..f39da8afe5 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/zenwithin/init.lua @@ -28,9 +28,9 @@ local HEATING_SETPOINT = "heatingSetpoint" local BAT_MIN = 3.4 -- voltage when device UI starts to die, ie, when battery fails local BAT_MAX = 6.0 -- 4 batteries at 1.5V (6.0V) -local DEFAULT_MIN_SETPOINT = 4.0 -local DEFAULT_MAX_SETPOINT = 37.5 - +local DEFAULT_MIN_SETPOINT = 400 -- 4.0C +local DEFAULT_MAX_SETPOINT = 3750 -- 37.5C +local FAHRENHEIT_THRESHOLD = 40 -- 40C -- In zenwithin sub driver, supported thermostat mode follow preference option because sensor always returns 'all mode possible' local SUPPORTED_THERMOSTAT_MODES = { [0x01] = { ModeAttribute.off.NAME, ModeAttribute.heat.NAME }, @@ -70,7 +70,7 @@ end local setpoint_limit_handler = function(limit_type) return function(driver, device, limit) - device:set_field(limit_type, limit.value / 100.0, {persist = true}) + device:set_field(limit_type, limit.value, {persist = true}) end end @@ -93,11 +93,12 @@ local update_device_setpoint = function(device) local current_mode = device:get_latest_state("main", ThermostatMode.ID, ModeAttribute.NAME) if (current_mode == ModeAttribute.heat.NAME or current_mode == ModeAttribute.emergency_heat.NAME) and cooling_setpoint ~= nil then - cooling_setpoint = device:get_latest_state("main", ThermostatCoolingSetpoint.ID, ThermostatCoolingSetpoint.coolingSetpoint.NAME) -- tried to set cooling setpoint while device was in heat mode + cooling_setpoint = device:get_latest_state("main", ThermostatCoolingSetpoint.ID, ThermostatCoolingSetpoint.coolingSetpoint.NAME) device:emit_event(ThermostatCoolingSetpoint.coolingSetpoint({value = cooling_setpoint, unit = "C"})) cooling_setpoint = nil elseif (current_mode == ModeAttribute.cool.NAME) and heating_setpoint ~= nil then + -- tried to set heating setpoint while device was in cool mode heating_setpoint = device:get_latest_state("main", ThermostatHeatingSetpoint.ID, ThermostatHeatingSetpoint.heatingSetpoint.NAME) device:emit_event(ThermostatHeatingSetpoint.heatingSetpoint({value = heating_setpoint, unit = "C"})) heating_setpoint = nil @@ -108,20 +109,21 @@ local update_device_setpoint = function(device) end if (heating_setpoint ~= nil) then - device:send(Thermostat.attributes.OccupiedHeatingSetpoint:write(device, utils.round(heating_setpoint*100))) + device:send(Thermostat.attributes.OccupiedHeatingSetpoint:write(device, utils.round(heating_setpoint))) end if (cooling_setpoint ~= nil) then - device:send(Thermostat.attributes.OccupiedCoolingSetpoint:write(device, utils.round(cooling_setpoint*100))) + device:send(Thermostat.attributes.OccupiedCoolingSetpoint:write(device, utils.round(cooling_setpoint))) end end local set_cooling_setpoint = function(driver, device, command) local value = command.args.setpoint - if value >= 40 then -- we got a command in fahrenheit + if value >= FAHRENHEIT_THRESHOLD then -- we got a command in fahrenheit value = utils.f_to_c(value) end - value = utils.clamp_value(value, + -- Scale up to centidegrees to align with the rest of the values + value = utils.clamp_value(value * 100, device:get_field(MIN_COOL_LIMIT) or DEFAULT_MIN_SETPOINT, device:get_field(MAX_COOL_LIMIT) or DEFAULT_MAX_SETPOINT) device:set_field(COOLING_SETPOINT, value) @@ -138,10 +140,11 @@ end local set_heating_setpoint = function(driver, device, command) local value = command.args.setpoint - if value >= 40 then -- we got a command in fahrenheit + if value >= FAHRENHEIT_THRESHOLD then -- we got a command in fahrenheit value = utils.f_to_c(value) end - value = utils.clamp_value(value, + -- Scale up to centidegrees to align with the rest of the values + value = utils.clamp_value(value * 100, device:get_field(MIN_HEAT_LIMIT) or DEFAULT_MIN_SETPOINT, device:get_field(MAX_HEAT_LIMIT) or DEFAULT_MAX_SETPOINT) device:set_field(HEATING_SETPOINT, value) From b6e09b301c8b6cd2279d0ab3dafa84408dd17433 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:48:40 -0500 Subject: [PATCH 053/102] Zwave Lock: Remove Migration on Added (#3127) * Zwave Lock: Remove Migration on Added * update and remove tests as needed --- drivers/SmartThings/zwave-lock/src/init.lua | 10 - .../src/test/test_init_lifecycle_handlers.lua | 63 ----- .../zwave-lock/src/test/test_keywe_lock.lua | 3 - .../zwave-lock/src/test/test_samsung_lock.lua | 217 --------------- .../zwave-lock/src/test/test_schlage_lock.lua | 263 ------------------ 5 files changed, 556 deletions(-) delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 6c85639625..b49489a008 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -15,7 +15,6 @@ local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local capabilities = require "st.capabilities" local consts = require "lock_utils.constants" -local lock_utils = require "lock_utils.utils" local table_utils = require "lock_utils.tables" local zwave_handlers = require "lock_handlers.zwave_responses" local capability_handlers = require "lock_handlers.capabilities" @@ -24,13 +23,6 @@ local capability_handlers = require "lock_handlers.capabilities" local LockLifecycle = {} function LockLifecycle.device_added(driver, device) - if device:supports_capability(capabilities.lockCodes) and device._provisioning_state == "TYPED" then - -- set the migrated field to true so new devices use lockCredentials/lockUsers from the start. - -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, - -- and should be migrated manually by the user. - device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) - end if device:supports_capability(capabilities.tamperAlert) then device:emit_event(capabilities.tamperAlert.tamper.clear()) end @@ -40,8 +32,6 @@ function LockLifecycle.device_added(driver, device) command = capabilities.refresh.commands.refresh.NAME, args = {} }) - -- ensure our user/credential state is accurate to the current device state - device.thread:call_with_delay(5, function() lock_utils.sync_device_state(device) end) end function LockLifecycle.init(driver, device) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua b/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua index ac127f234a..d521e2d3a0 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua @@ -38,13 +38,6 @@ local mock_device_base = test.mock_device.build_test_zwave_device({ zwave_endpoints = zwave_lock_endpoints, }) --- Same profile but provisioning_state = "TYPED" (freshly fingerprinted) -local mock_device_typed = test.mock_device.build_test_zwave_device({ - profile = t_utils.get_profile_definition("base-lock.yml"), - zwave_endpoints = zwave_lock_endpoints, - _provisioning_state = "TYPED", -}) - -- lock-battery profile: lock + battery only (no lockCodes / lockCredentials) local mock_device_battery = test.mock_device.build_test_zwave_device({ profile = t_utils.get_profile_definition("lock-battery.yml"), @@ -62,31 +55,6 @@ end -- added (device_added) -- ============================================================================ -test.register_coroutine_test( - "added: TYPED device with lockCodes emits migrated event, persists SLGA_MIGRATED, and injects refresh", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "added" }) - - -- Migrated event emitted for TYPED+lockCodes device - test.socket.capability:__expect_send( - mock_device_typed:generate_test_message("main", - capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - ) - -- inject_capability_command calls the refresh handler: - -- DoorLock:OperationGet, Battery:Get, UserCode:UsersNumberGet (no cached values) - test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device_typed.id)) - test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device_typed.id)) - test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device_typed.id)) - test.wait_for_events() - - assert( - mock_device_typed:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true, - "SLGA_MIGRATED must be true after added fires for a TYPED device" - ) - end, - { test_init = make_test_init(mock_device_typed) } -) - test.register_coroutine_test( "added: non-TYPED (PROVISIONED) device with lockCodes does NOT emit migrated but still injects refresh", function() @@ -163,36 +131,5 @@ test.register_coroutine_test( { test_init = make_test_init(mock_device_battery) } ) --- ============================================================================ --- driverSwitched (LockLifecycle.driver_switched) --- ============================================================================ - -test.register_coroutine_test( - "driver_switched: device with lockCodes and migrated=true persists SLGA_MIGRATED and updates metadata", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "added" }) - - -- Migrated event emitted for TYPED+lockCodes device - test.socket.capability:__expect_send( - mock_device_typed:generate_test_message("main", - capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - ) - -- inject_capability_command calls the refresh handler: - -- DoorLock:OperationGet, Battery:Get, UserCode:UsersNumberGet (no cached values) - test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device_typed.id)) - test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device_typed.id)) - test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device_typed.id)) - test.wait_for_events() - - -- driverSwitched occurs after added, so migrated=true is already set in the capability state cache - test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "driver_switched" }) - mock_device_typed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.wait_for_events() - - assert(mock_device_typed:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true) - end, - { test_init = make_test_init(mock_device_typed) } -) - -- ============================================================================ test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index f6554141b0..6724094462 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -9,7 +9,6 @@ local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) -local lock_utils = require "lock_utils.constants".DRIVER_STATE test.disable_startup_messages() @@ -45,7 +44,6 @@ test.set_test_init_function(test_init) local function added() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) test.socket.zwave:__expect_send( DoorLock:OperationGet({}):build_test_tx(mock_device.id) @@ -58,7 +56,6 @@ local function added() ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) test.wait_for_events() - assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") end test.register_coroutine_test( diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua deleted file mode 100644 index 532e1da1a9..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ /dev/null @@ -1,217 +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 zw_test_utils = require "integration_test.zwave_test_utils" -local t_utils = require "integration_test.utils" -local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({version=1}) -local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) -local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local lock_utils = require "lock_utils.constants".DRIVER_STATE - -test.disable_startup_messages() - -local SAMSUNG_MANUFACTURER_ID = 0x022E -local SAMSUNG_PRODUCT_TYPE = 0x0001 -local SAMSUNG_PRODUCT_ID = 0x0001 - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock.yml"), - _provisioning_state = "TYPED", - zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, - zwave_product_type = SAMSUNG_PRODUCT_TYPE, - zwave_product_id = SAMSUNG_PRODUCT_ID, - } -) - -local function test_init() - test.mock_device.add_test_device(mock_device) -end - -test.set_test_init_function(test_init) - -local function added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) - - test.socket.zwave:__expect_send( - DoorLock:OperationGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - Battery:Get({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - test.wait_for_events() - assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") -end - -test.register_coroutine_test( - "When the device is added an unlocked event should be sent", - function() - added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Setting a user code name should be handled", - function() - added() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_ADDED, - event_parameter = "" } - ) - }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Get({user_identifier = 1}) - ) - ) - test.socket.zwave:__queue_receive({ - mock_device.id, - UserCode:Report({ - user_identifier = 1, - user_code = "1234", - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "Notification about correctly added code should be handled", - function() - added() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_ADDED, - event_parameter = string.char(1) - }) - }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Get({user_identifier = 1}) - ) - ) - end -) - -test.register_coroutine_test( - "All user codes should be reported as deleted upon changing Master Code", - function() - added() - mock_device:set_field("persistedUsers", { - { userIndex = 1, userName = "Code 1", userType = "guest" }, - { userIndex = 2, userName = "Code 2", userType = "guest" }, - { userIndex = 3, userName = "Code 3", userType = "guest" }, - }, { persist = true }) - mock_device:set_field("persistedCredentials", { - { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, - { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, - { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, - }, { persist = true }) - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "updateUser", - args = {1, "new name", "guest" } - }, - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "updateUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({ - { userIndex = 1, userName = "new name", userType = "guest" }, - { userIndex = 2, userName = "Code 2", userType = "guest" }, - { userIndex = 3, userName = "Code 3", userType = "guest" } - }, - { visibility = { displayed = false } }) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, - event_parameter = "" } - ) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { visibility = { displayed = false } }) - ) - ) - end -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua deleted file mode 100644 index 90d919c27e..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ /dev/null @@ -1,263 +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 zw = require "st.zwave" -local zw_test_utils = require "integration_test.zwave_test_utils" -local t_utils = require "integration_test.utils" -local lock_utils = require "lock_utils.constants".DRIVER_STATE - -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) -local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) -local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) - -test.disable_startup_messages() - -local SCHLAGE_MANUFACTURER_ID = 0x003B -local SCHLAGE_PRODUCT_TYPE = 0x0002 -local SCHLAGE_PRODUCT_ID = 0x0469 - --- supported comand classes -local zwave_lock_endpoints = { - { - command_classes = { - {value = zw.BATTERY}, - {value = zw.DOOR_LOCK}, - {value = zw.USER_CODE}, - {value = zw.NOTIFICATION} - } - } -} - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock.yml"), - _provisioning_state = "TYPED", - zwave_endpoints = zwave_lock_endpoints, - zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, - zwave_product_type = SCHLAGE_PRODUCT_TYPE, - zwave_product_id = SCHLAGE_PRODUCT_ID, - } -) - -local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} - -local function test_init() - test.mock_device.add_test_device(mock_device) -end - -test.set_test_init_function(test_init) - -local function added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) - - test.socket.zwave:__expect_send( - DoorLock:OperationGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - Battery:Get({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - test.wait_for_events() - assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") -end - -test.register_coroutine_test( - "Setting a user code should result in the named code changed event firing", - function() - added() - test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number}) - ) - ) - test.wait_for_events() - test.mock_time.advance_time(4.2) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.STATUS_NOT_AVAILABLE, user_code="0000\n\r"}) }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "Configuration report should be handled", - function() - added() - test.socket.zwave:__queue_receive({ - mock_device.id, - Configuration:Report({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = 6 - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) - ) - end -) - -test.register_coroutine_test( - "Configuration report indicating code deletion should be handled", - function() - added() - test.socket.zwave:__queue_receive({ - mock_device.id, - Configuration:Report({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = 6 - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_device.id, - Configuration:Report({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = 4 - }) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(4)) - ) - end -) - -test.register_coroutine_test( - "User code report indicating master code is available should indicate code deletion", - function() - added() - test.socket.zwave:__queue_receive({ - mock_device.id, - UserCode:Report({ - user_identifier = 0, - user_id_status = UserCode.user_id_status.AVAILABLE - }) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { visibility = { displayed = false } }) - ) - ) - end -) - -test.register_coroutine_test( - "Device should send appropriate configuration messages", - function() - added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Configuration:Get({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number - }) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Association:Set({ - grouping_identifier = 2, - node_ids = {} - }) - ) - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Basic Sets should result in an Association remove", - function() - added() - test.socket.zwave:__queue_receive({ - mock_device.id, - Basic:Set({ - value = 0x00 - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({})) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Association:Remove({ - grouping_identifier = 1, - node_ids = {} - }) - ) - ) - end -) - -test.run_registered_tests() From 012d4086f203286cef531101cd4ea232da8a06a8 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:48:40 -0500 Subject: [PATCH 054/102] Zwave Lock: Remove Migration on Added (#3127) * Zwave Lock: Remove Migration on Added * update and remove tests as needed --- drivers/SmartThings/zwave-lock/src/init.lua | 10 - .../src/test/test_init_lifecycle_handlers.lua | 63 ----- .../zwave-lock/src/test/test_keywe_lock.lua | 3 - .../zwave-lock/src/test/test_samsung_lock.lua | 217 --------------- .../zwave-lock/src/test/test_schlage_lock.lua | 263 ------------------ 5 files changed, 556 deletions(-) delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua delete mode 100644 drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua diff --git a/drivers/SmartThings/zwave-lock/src/init.lua b/drivers/SmartThings/zwave-lock/src/init.lua index 6c85639625..b49489a008 100644 --- a/drivers/SmartThings/zwave-lock/src/init.lua +++ b/drivers/SmartThings/zwave-lock/src/init.lua @@ -15,7 +15,6 @@ local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) local capabilities = require "st.capabilities" local consts = require "lock_utils.constants" -local lock_utils = require "lock_utils.utils" local table_utils = require "lock_utils.tables" local zwave_handlers = require "lock_handlers.zwave_responses" local capability_handlers = require "lock_handlers.capabilities" @@ -24,13 +23,6 @@ local capability_handlers = require "lock_handlers.capabilities" local LockLifecycle = {} function LockLifecycle.device_added(driver, device) - if device:supports_capability(capabilities.lockCodes) and device._provisioning_state == "TYPED" then - -- set the migrated field to true so new devices use lockCredentials/lockUsers from the start. - -- auto-migration is only run for typed devices, as provisioned devices have already been onboarded, - -- and should be migrated manually by the user. - device:emit_event(capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - device:set_field(consts.DRIVER_STATE.SLGA_MIGRATED, true, { persist = true }) - end if device:supports_capability(capabilities.tamperAlert) then device:emit_event(capabilities.tamperAlert.tamper.clear()) end @@ -40,8 +32,6 @@ function LockLifecycle.device_added(driver, device) command = capabilities.refresh.commands.refresh.NAME, args = {} }) - -- ensure our user/credential state is accurate to the current device state - device.thread:call_with_delay(5, function() lock_utils.sync_device_state(device) end) end function LockLifecycle.init(driver, device) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua b/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua index ac127f234a..d521e2d3a0 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_init_lifecycle_handlers.lua @@ -38,13 +38,6 @@ local mock_device_base = test.mock_device.build_test_zwave_device({ zwave_endpoints = zwave_lock_endpoints, }) --- Same profile but provisioning_state = "TYPED" (freshly fingerprinted) -local mock_device_typed = test.mock_device.build_test_zwave_device({ - profile = t_utils.get_profile_definition("base-lock.yml"), - zwave_endpoints = zwave_lock_endpoints, - _provisioning_state = "TYPED", -}) - -- lock-battery profile: lock + battery only (no lockCodes / lockCredentials) local mock_device_battery = test.mock_device.build_test_zwave_device({ profile = t_utils.get_profile_definition("lock-battery.yml"), @@ -62,31 +55,6 @@ end -- added (device_added) -- ============================================================================ -test.register_coroutine_test( - "added: TYPED device with lockCodes emits migrated event, persists SLGA_MIGRATED, and injects refresh", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "added" }) - - -- Migrated event emitted for TYPED+lockCodes device - test.socket.capability:__expect_send( - mock_device_typed:generate_test_message("main", - capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - ) - -- inject_capability_command calls the refresh handler: - -- DoorLock:OperationGet, Battery:Get, UserCode:UsersNumberGet (no cached values) - test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device_typed.id)) - test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device_typed.id)) - test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device_typed.id)) - test.wait_for_events() - - assert( - mock_device_typed:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true, - "SLGA_MIGRATED must be true after added fires for a TYPED device" - ) - end, - { test_init = make_test_init(mock_device_typed) } -) - test.register_coroutine_test( "added: non-TYPED (PROVISIONED) device with lockCodes does NOT emit migrated but still injects refresh", function() @@ -163,36 +131,5 @@ test.register_coroutine_test( { test_init = make_test_init(mock_device_battery) } ) --- ============================================================================ --- driverSwitched (LockLifecycle.driver_switched) --- ============================================================================ - -test.register_coroutine_test( - "driver_switched: device with lockCodes and migrated=true persists SLGA_MIGRATED and updates metadata", - function() - test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "added" }) - - -- Migrated event emitted for TYPED+lockCodes device - test.socket.capability:__expect_send( - mock_device_typed:generate_test_message("main", - capabilities.lockCodes.migrated(true, { visibility = { displayed = false } })) - ) - -- inject_capability_command calls the refresh handler: - -- DoorLock:OperationGet, Battery:Get, UserCode:UsersNumberGet (no cached values) - test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device_typed.id)) - test.socket.zwave:__expect_send(Battery:Get({}):build_test_tx(mock_device_typed.id)) - test.socket.zwave:__expect_send(UserCode:UsersNumberGet({}):build_test_tx(mock_device_typed.id)) - test.wait_for_events() - - -- driverSwitched occurs after added, so migrated=true is already set in the capability state cache - test.socket.device_lifecycle:__queue_receive({ mock_device_typed.id, "driver_switched" }) - mock_device_typed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - test.wait_for_events() - - assert(mock_device_typed:get_field(constants.DRIVER_STATE.SLGA_MIGRATED) == true) - end, - { test_init = make_test_init(mock_device_typed) } -) - -- ============================================================================ test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index f6554141b0..6724094462 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -9,7 +9,6 @@ local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) -local lock_utils = require "lock_utils.constants".DRIVER_STATE test.disable_startup_messages() @@ -45,7 +44,6 @@ test.set_test_init_function(test_init) local function added() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) test.socket.zwave:__expect_send( DoorLock:OperationGet({}):build_test_tx(mock_device.id) @@ -58,7 +56,6 @@ local function added() ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) test.wait_for_events() - assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") end test.register_coroutine_test( diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua deleted file mode 100644 index 532e1da1a9..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock.lua +++ /dev/null @@ -1,217 +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 zw_test_utils = require "integration_test.zwave_test_utils" -local t_utils = require "integration_test.utils" -local UserCode = (require "st.zwave.CommandClass.UserCode")({version=1}) -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({version=1}) -local Battery = (require "st.zwave.CommandClass.Battery")({version=1}) -local Notification = (require "st.zwave.CommandClass.Notification")({version=3}) -local lock_utils = require "lock_utils.constants".DRIVER_STATE - -test.disable_startup_messages() - -local SAMSUNG_MANUFACTURER_ID = 0x022E -local SAMSUNG_PRODUCT_TYPE = 0x0001 -local SAMSUNG_PRODUCT_ID = 0x0001 - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock.yml"), - _provisioning_state = "TYPED", - zwave_manufacturer_id = SAMSUNG_MANUFACTURER_ID, - zwave_product_type = SAMSUNG_PRODUCT_TYPE, - zwave_product_id = SAMSUNG_PRODUCT_ID, - } -) - -local function test_init() - test.mock_device.add_test_device(mock_device) -end - -test.set_test_init_function(test_init) - -local function added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) - - test.socket.zwave:__expect_send( - DoorLock:OperationGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - Battery:Get({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - test.wait_for_events() - assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") -end - -test.register_coroutine_test( - "When the device is added an unlocked event should be sent", - function() - added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.lock.unlocked()) - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Setting a user code name should be handled", - function() - added() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_ADDED, - event_parameter = "" } - ) - }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Get({user_identifier = 1}) - ) - ) - test.socket.zwave:__queue_receive({ - mock_device.id, - UserCode:Report({ - user_identifier = 1, - user_code = "1234", - user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS - }) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "Notification about correctly added code should be handled", - function() - added() - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_USER_CODE_ADDED, - event_parameter = string.char(1) - }) - }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Get({user_identifier = 1}) - ) - ) - end -) - -test.register_coroutine_test( - "All user codes should be reported as deleted upon changing Master Code", - function() - added() - mock_device:set_field("persistedUsers", { - { userIndex = 1, userName = "Code 1", userType = "guest" }, - { userIndex = 2, userName = "Code 2", userType = "guest" }, - { userIndex = 3, userName = "Code 3", userType = "guest" }, - }, { persist = true }) - mock_device:set_field("persistedCredentials", { - { userIndex = 1, credentialIndex = 1, credentialType = "pin" }, - { userIndex = 2, credentialIndex = 2, credentialType = "pin" }, - { userIndex = 3, credentialIndex = 3, credentialType = "pin" }, - }, { persist = true }) - test.socket.capability:__queue_receive({ - mock_device.id, - { - capability = capabilities.lockUsers.ID, - command = "updateUser", - args = {1, "new name", "guest" } - }, - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.commandResult( - { commandName = "updateUser", statusCode = "success", userIndex = 1 }, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({ - { userIndex = 1, userName = "new name", userType = "guest" }, - { userIndex = 2, userName = "Code 2", userType = "guest" }, - { userIndex = 3, userName = "Code 3", userType = "guest" } - }, - { visibility = { displayed = false } }) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_device.id, - Notification:Report({ - notification_type = Notification.notification_type.ACCESS_CONTROL, - event = Notification.event.access_control.NEW_PROGRAM_CODE_ENTERED_UNIQUE_CODE_FOR_LOCK_CONFIGURATION, - event_parameter = "" } - ) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { visibility = { displayed = false } }) - ) - ) - end -) - -test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua deleted file mode 100644 index 90d919c27e..0000000000 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock.lua +++ /dev/null @@ -1,263 +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 zw = require "st.zwave" -local zw_test_utils = require "integration_test.zwave_test_utils" -local t_utils = require "integration_test.utils" -local lock_utils = require "lock_utils.constants".DRIVER_STATE - -local UserCode = (require "st.zwave.CommandClass.UserCode")({ version = 1 }) -local Configuration = (require "st.zwave.CommandClass.Configuration")({ version = 2 }) -local Association = (require "st.zwave.CommandClass.Association")({ version = 1 }) -local Basic = (require "st.zwave.CommandClass.Basic")({ version = 1 }) -local Battery = (require "st.zwave.CommandClass.Battery")({ version = 1 }) -local DoorLock = (require "st.zwave.CommandClass.DoorLock")({ version = 1 }) - -test.disable_startup_messages() - -local SCHLAGE_MANUFACTURER_ID = 0x003B -local SCHLAGE_PRODUCT_TYPE = 0x0002 -local SCHLAGE_PRODUCT_ID = 0x0469 - --- supported comand classes -local zwave_lock_endpoints = { - { - command_classes = { - {value = zw.BATTERY}, - {value = zw.DOOR_LOCK}, - {value = zw.USER_CODE}, - {value = zw.NOTIFICATION} - } - } -} - -local mock_device = test.mock_device.build_test_zwave_device( - { - profile = t_utils.get_profile_definition("base-lock.yml"), - _provisioning_state = "TYPED", - zwave_endpoints = zwave_lock_endpoints, - zwave_manufacturer_id = SCHLAGE_MANUFACTURER_ID, - zwave_product_type = SCHLAGE_PRODUCT_TYPE, - zwave_product_id = SCHLAGE_PRODUCT_ID, - } -) - -local SCHLAGE_LOCK_CODE_LENGTH_PARAM = {number = 16, size = 1} - -local function test_init() - test.mock_device.add_test_device(mock_device) -end - -test.set_test_init_function(test_init) - -local function added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true, { visibility = { displayed = false } }))) - - test.socket.zwave:__expect_send( - DoorLock:OperationGet({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - Battery:Get({}):build_test_tx(mock_device.id) - ) - test.socket.zwave:__expect_send( - UserCode:UsersNumberGet({}):build_test_tx(mock_device.id) - ) - test.wait_for_events() - assert(mock_device:get_field(lock_utils.SLGA_MIGRATED) == true, "Device should be marked as migrated") -end - -test.register_coroutine_test( - "Setting a user code should result in the named code changed event firing", - function() - added() - test.timer.__create_and_queue_test_time_advance_timer(4.2, "oneshot") - test.socket.capability:__queue_receive({ mock_device.id, { capability = capabilities.lockCredentials.ID, command = "addCredential", args = { 0, "guest", "pin", "1234"} } }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Configuration:Get({parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number}) - ) - ) - test.wait_for_events() - test.mock_time.advance_time(4.2) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - UserCode:Set({user_identifier = 1, user_code = "1234", user_id_status = UserCode.user_id_status.ENABLED_GRANT_ACCESS}) - ) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({mock_device.id, UserCode:Report({user_identifier = 1, user_id_status = UserCode.user_id_status.STATUS_NOT_AVAILABLE, user_code="0000\n\r"}) }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({{ userIndex = 1, credentialIndex = 1, credentialType = "pin" }}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.commandResult( - { commandName = "addCredential", statusCode = "success", credentialIndex = 1, userIndex = 1}, - { state_change = true, visibility = { displayed = false } } - ) - ) - ) - end -) - -test.register_coroutine_test( - "Configuration report should be handled", - function() - added() - test.socket.zwave:__queue_receive({ - mock_device.id, - Configuration:Report({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = 6 - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) - ) - end -) - -test.register_coroutine_test( - "Configuration report indicating code deletion should be handled", - function() - added() - test.socket.zwave:__queue_receive({ - mock_device.id, - Configuration:Report({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = 6 - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(6)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(6)) - ) - test.wait_for_events() - test.socket.zwave:__queue_receive({ - mock_device.id, - Configuration:Report({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number, - configuration_value = 4 - }) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.minPinCodeLen(4)) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCredentials.maxPinCodeLen(4)) - ) - end -) - -test.register_coroutine_test( - "User code report indicating master code is available should indicate code deletion", - function() - added() - test.socket.zwave:__queue_receive({ - mock_device.id, - UserCode:Report({ - user_identifier = 0, - user_id_status = UserCode.user_id_status.AVAILABLE - }) - }) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users({}, - { visibility = { displayed = false } }) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials({}, - { visibility = { displayed = false } }) - ) - ) - end -) - -test.register_coroutine_test( - "Device should send appropriate configuration messages", - function() - added() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Configuration:Get({ - parameter_number = SCHLAGE_LOCK_CODE_LENGTH_PARAM.number - }) - ) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Association:Set({ - grouping_identifier = 2, - node_ids = {} - }) - ) - ) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "Basic Sets should result in an Association remove", - function() - added() - test.socket.zwave:__queue_receive({ - mock_device.id, - Basic:Set({ - value = 0x00 - }) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.lock.unlocked({})) - ) - test.socket.zwave:__expect_send( - zw_test_utils.zwave_test_build_send_command( - mock_device, - Association:Remove({ - grouping_identifier = 1, - node_ids = {} - }) - ) - ) - end -) - -test.run_registered_tests() From 834ee18ae6001d2437b8a3f9cf60ba7894f85cc1 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 17 Jul 2026 09:52:46 -0500 Subject: [PATCH 055/102] zigbee-range-extender: guard against nil device in health check Driver:get_device_info can return (nil, err) when the devices API request fails or the device is no longer known to the driver (e.g. transient device_api error, or a device removed between building the device list and looking it up). device_health_check ignored the second return value and unconditionally called device:send(...), which internally calls device:get_short_address() in cluster_base.lua, causing 'attempt to call a nil value (method get_short_address)' when device was nil. Impact: the periodic health-check coroutine crashes with an unhandled error, which can disrupt the scheduled health-check loop and spam hub logs, potentially leaving range extender devices marked unhealthy/unresponsive even though the actual issue was just a lookup failure. Fix: check the return value of get_device_info and skip sending the ZCLVersion read (logging a warning) when the device could not be resolved, instead of dereferencing nil. Co-Authored-By: OpenCode --- drivers/SmartThings/zigbee-range-extender/src/init.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-range-extender/src/init.lua b/drivers/SmartThings/zigbee-range-extender/src/init.lua index f6ba3c1ffc..f06988f5cf 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/init.lua @@ -3,6 +3,7 @@ local capabilities = require "st.capabilities" +local log = require "log" local defaults = require "st.zigbee.defaults" local Basic = (require "st.zigbee.zcl.clusters").Basic local ZigbeeDriver = require "st.zigbee" @@ -33,8 +34,13 @@ local zigbee_range_extender_driver = ZigbeeDriver("zigbee-range-extender", zigbe function zigbee_range_extender_driver:device_health_check() local device_list = self.device_api.get_device_list() for _, device_id in ipairs(device_list) do - local device = self:get_device_info(device_id, false) - device:send(Basic.attributes.ZCLVersion:read(device)) + local device, err = self:get_device_info(device_id, false) + if device == nil then + log.warn_with({ hub_logs = true }, + string.format("device_health_check failed to get device info for device_id %s: %s", device_id, err)) + else + device:send(Basic.attributes.ZCLVersion:read(device)) + end end end From e6811a1f2fac309567d851d5e93dd769a0bced34 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 17 Jul 2026 09:52:47 -0500 Subject: [PATCH 056/102] jbl: fix coroutine crash from concatenating nil credential in add_header update_connection() fetched the CREDENTIAL device field and passed it directly to jbl_api:add_header without checking for nil, unlike the sibling call site in device_added(). When a device reconnects via find_new_connection() before a credential has been stored on the device, add_header's string concatenation ('.. .. value') throws 'attempt to concatenate a nil value (local value)', which crashes the coroutine handling the device and can leave the device connection in a bad state (no SSE/health-check updates) until the driver restarts. Fix: - Guard update_connection so it logs the missing credential, marks the device offline, and returns early instead of reaching add_header with a nil value. - Harden jbl_api:add_header to tostring() its key/value before concatenation as defense-in-depth, so any other future caller can't trigger the same crash. Co-Authored-By: OpenCode --- drivers/SmartThings/jbl/src/init.lua | 5 +++++ drivers/SmartThings/jbl/src/jbl/api.lua | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/jbl/src/init.lua b/drivers/SmartThings/jbl/src/init.lua index f422ecf84c..830a98a181 100644 --- a/drivers/SmartThings/jbl/src/init.lua +++ b/drivers/SmartThings/jbl/src/init.lua @@ -82,6 +82,11 @@ local function update_connection(driver, device, device_ip, device_info) local conn_info = driver.discovery_helper.get_connection_info(driver, device_dni, device_ip, device_info) local credential = device:get_field(fields.CREDENTIAL) + if not credential then + log.error("update_connection : failed to find credential, dni = " .. tostring(device_dni)) + device:offline() + return + end conn_info:add_header(CREDENTIAL_KEY_HEADER, credential) diff --git a/drivers/SmartThings/jbl/src/jbl/api.lua b/drivers/SmartThings/jbl/src/jbl/api.lua index 95e25d14c3..7479289549 100644 --- a/drivers/SmartThings/jbl/src/jbl/api.lua +++ b/drivers/SmartThings/jbl/src/jbl/api.lua @@ -77,7 +77,7 @@ function jbl_api.new_device_manager(bridge_ip, bridge_info, socket_builder) end function jbl_api:add_header(key, value) - log.info("add_header : " .. key .. ", " .. value) + log.info("add_header : " .. tostring(key) .. ", " .. tostring(value)) self.headers[key] = value end From 57ae75d26dbd59d9bab3f695887a2551bc28ae2e Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 17 Jul 2026 09:53:40 -0500 Subject: [PATCH 057/102] aqara-presence-sensor: guard against receive(0) crashing SSE reader The Aqara Presence Sensor FP2 driver uses an SSE/HTTP chunked-transfer client (lunchbox/sse/eventsource.lua) to stream events from the device. When a chunk-encoded response ends, the server sends a zero-length chunk header ("0\r\n"). The reader parsed this as recv_as_num == 0 and called sock:receive(0), which the shared cosock TCP socket (cosock/socket/tcp.lua) does not handle gracefully: its output transform asserts that the received data is non-nil, so a 0-byte receive raises "socket receive returned nil data" instead of returning a normal result. That assertion failure crashes the coroutine driving the device's LAN connection, which drops presence updates until the hub restarts the driver. Fix: when the parsed chunk size is 0, skip the socket receive call entirely and treat it as an empty read, then continue on to consume the trailing CRLF as before. This avoids ever calling receive(0) on the socket, preventing the crash while preserving existing timeout/ closed/error handling paths. Co-Authored-By: OpenCode --- .../src/lunchbox/sse/eventsource.lua | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/drivers/Aqara/aqara-presence-sensor/src/lunchbox/sse/eventsource.lua b/drivers/Aqara/aqara-presence-sensor/src/lunchbox/sse/eventsource.lua index 7c72f2d81a..9b64844821 100644 --- a/drivers/Aqara/aqara-presence-sensor/src/lunchbox/sse/eventsource.lua +++ b/drivers/Aqara/aqara-presence-sensor/src/lunchbox/sse/eventsource.lua @@ -365,18 +365,26 @@ local function open_action(source) local recv_as_num = tonumber(recv, 16) if recv_as_num ~= nil then - recv, err, partial = source._sock:receive(recv_as_num) - if err then - if err == "timeout" or err == "wantread" then - return - else - --- real error, close the connection. - if source._sock ~= nil then - source._sock:close() - source._sock = nil + -- A chunk size of 0 signals the end of the chunked transfer. Some + -- versions of the underlying socket library will raise a hard error + -- (rather than returning a graceful nil/"closed" pair) if we ask to + -- receive 0 bytes, so avoid calling receive() in that case entirely. + if recv_as_num == 0 then + recv = "" + else + recv, err, partial = source._sock:receive(recv_as_num) + if err then + if err == "timeout" or err == "wantread" then + return + else + --- real error, close the connection. + if source._sock ~= nil then + source._sock:close() + source._sock = nil + end + source.ready_state = EventSource.ReadyStates.CLOSED + return nil, err, partial end - source.ready_state = EventSource.ReadyStates.CLOSED - return nil, err, partial end end local _, err, partial = source._sock:receive('*l') -- clear the final line From 9d7daafdce5b26a9944690b6a31fdcc29c9c8024 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 17 Jul 2026 09:54:08 -0500 Subject: [PATCH 058/102] zwave-sensor: guard tamper-clear timer against device removal The timed-tamper-clear sub-driver schedules a delayed callback (via device.thread:call_with_delay) to emit a tamperAlert clear event a fixed time after a tamper notification is received. If the device is removed from the hub before that timer fires, st.device's Device:deleted() swaps the device's metatable so that every field and method lookup returns nil. When the pending timer callback later runs and calls device:emit_event_for_endpoint(...), that method resolves to nil, producing: attempt to call a nil value (method 'emit_event_for_endpoint') This crashed the device's Lua thread coroutine, so any other pending work for that device (or shared device thread, since this driver uses shared_device_thread_enabled) could be disrupted, degrading the user experience for other devices using the same driver/thread. Fix: check device.id (which becomes nil once the device is deleted) before touching the device inside the delayed callback, and skip the event emission/timer cleanup if the device is no longer valid. Co-Authored-By: OpenCode --- .../zwave-sensor/src/timed-tamper-clear/init.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua index 8554dab28f..dd6cc672f4 100644 --- a/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua +++ b/drivers/SmartThings/zwave-sensor/src/timed-tamper-clear/init.lua @@ -22,6 +22,13 @@ local function handle_tamper_event(driver, device, cmd) device.thread:cancel_timer(tamper_timer) end device:set_field(TAMPER_TIMER, device.thread:call_with_delay(TAMPER_CLEAR, function() + -- The device can be removed while this timer is still pending. Once a device is + -- deleted its metatable is swapped so that all field/method lookups return nil + -- (see Device:deleted() in st.device), so guard against that here to avoid + -- "attempt to call a nil value (method 'emit_event_for_endpoint')" errors. + if device.id == nil then + return + end device:emit_event_for_endpoint(cmd.src_channel, capabilities.tamperAlert.tamper.clear()) device:set_field(TAMPER_TIMER, nil) end)) From e53691e9f6c10253a525263b54ace6e06177f561 Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 17 Jul 2026 09:56:57 -0500 Subject: [PATCH 059/102] zigbee-switch(inovelli): guard nil parent device in child notification handlers Child (notification) devices call device:get_parent_device() and then schedule a 1s-delayed closure that sends a manufacturer-specific command via the captured parent reference. If the parent device is removed (or otherwise not resolvable, e.g. during a join/removal race) at command time, get_parent_device() returns nil. The delayed closure then invokes cluster_base.build_manufacturer_specific_command(nil, ...), which calls device:get_short_address() on nil, crashing the coroutine with: attempt to call a nil value (method 'get_short_address') This meant on/off/level/color/color-temperature commands on an Inovelli notification child device could silently crash the driver (breaking further command handling) instead of just failing to notify the parent. Fix: check the parent device reference for nil immediately after get_parent_device() and bail out before scheduling the send, in on_handler, off_handler, switch_level_handler, set_color_temperature, and set_color. Also updated getNotificationValue to accept the already resolved parent device instead of re-fetching it, avoiding a second nil dereference of the same kind. Co-Authored-By: OpenCode --- .../zigbee-switch/src/inovelli/init.lua | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index a83a343663..97cd33e821 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -258,11 +258,11 @@ local function huePercentToValue(value) else return utils.round(value / 100 * 255) end end -local function getNotificationValue(device, value) +local function getNotificationValue(device, parent, value) local notificationValue = 0 local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) - local effect = device:get_parent_device().preferences.notificationType or 1 + local effect = (parent and parent.preferences.notificationType) or 1 notificationValue = notificationValue + (effect*16777216) notificationValue = notificationValue + (huePercentToValue(value or color)*65536) notificationValue = notificationValue + (level*256) @@ -276,13 +276,16 @@ local function on_handler(driver, device, command) else device:emit_event(capabilities.switch.switch("on")) local dev = device:get_parent_device() + if dev == nil then + return + end local send_configuration = function() dev:send(cluster_base.build_manufacturer_specific_command( dev, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) + utils.serialize_int(getNotificationValue(device, dev),4,false,false))) end device.thread:call_with_delay(1,send_configuration) end @@ -294,6 +297,9 @@ local function on_handler(driver, device, command) else device:emit_event(capabilities.switch.switch("off")) local dev = device:get_parent_device() + if dev == nil then + return + end local send_configuration = function() dev:send(cluster_base.build_manufacturer_specific_command( dev, @@ -313,13 +319,16 @@ local function switch_level_handler(driver, device, command) device:emit_event(capabilities.switchLevel.level(command.args.level)) device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) local dev = device:get_parent_device() + if dev == nil then + return + end local send_configuration = function() dev:send(cluster_base.build_manufacturer_specific_command( dev, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) + utils.serialize_int(getNotificationValue(device, dev),4,false,false))) end device.thread:call_with_delay(1,send_configuration) end @@ -330,13 +339,16 @@ local function set_color_temperature(driver, device, command) device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) device:emit_event(capabilities.switch.switch("on")) local dev = device:get_parent_device() + if dev == nil then + return + end local send_configuration = function() dev:send(cluster_base.build_manufacturer_specific_command( dev, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, - utils.serialize_int(getNotificationValue(device, 100),4,false,false))) + utils.serialize_int(getNotificationValue(device, dev, 100),4,false,false))) end device.thread:call_with_delay(1,send_configuration) end @@ -346,13 +358,16 @@ local function set_color_temperature(driver, device, command) device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) device:emit_event(capabilities.switch.switch("on")) local dev = device:get_parent_device() + if dev == nil then + return + end local send_configuration = function() dev:send(cluster_base.build_manufacturer_specific_command( dev, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) + utils.serialize_int(getNotificationValue(device, dev),4,false,false))) end device.thread:call_with_delay(1,send_configuration) end From 93d2b3527b070fe23bd95afb064ae30a10de797e Mon Sep 17 00:00:00 2001 From: cjswedes Date: Fri, 17 Jul 2026 09:57:26 -0500 Subject: [PATCH 060/102] matter-lock: guard nil capability event for unmapped LockState value Co-Authored-By: OpenCode --- drivers/SmartThings/matter-lock/src/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/SmartThings/matter-lock/src/init.lua b/drivers/SmartThings/matter-lock/src/init.lua index 5d82801b67..f94702c496 100755 --- a/drivers/SmartThings/matter-lock/src/init.lua +++ b/drivers/SmartThings/matter-lock/src/init.lua @@ -436,10 +436,10 @@ local function lock_state_handler(driver, device, ib, response) -- In this case, two events occur. To prevent this, when both functions are called, -- it send the event after 1 second so that no event occurs in the lock_state_handler. device.thread:call_with_delay(1, function () - if ib.data.value ~= nil then + if ib.data.value ~= nil and LOCK_STATE[ib.data.value] ~= nil then device:emit_event(LOCK_STATE[ib.data.value]) else - device.log.warn("Lock State is nil") + device.log.warn(string.format("Received unknown Lock State: %s", ib.data.value)) end end) end From 4e3d3b0ae018b86fdc62dcebd9c6b8dc6ded6acd Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Mon, 20 Jul 2026 10:58:38 -0500 Subject: [PATCH 061/102] CHAD-18838: Exclude `zoneManagement` when `USERDEFINED` feature is absent (#3126) --- .../camera_utils/device_configuration.lua | 5 +- .../src/test/test_matter_camera.lua | 180 +++++++++++++++--- 2 files changed, 161 insertions(+), 24 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua index 398150d063..0619194926 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua @@ -259,8 +259,9 @@ function CameraDeviceConfiguration.match_profile(device) clus_has_feature(clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then table.insert(main_component_capabilities, capabilities.mechanicalPanTiltZoom.ID) end - elseif ep_cluster.cluster_id == clusters.ZoneManagement.ID and has_server_cluster_type(ep_cluster) then - table.insert(main_component_capabilities, capabilities.zoneManagement.ID) + elseif ep_cluster.cluster_id == clusters.ZoneManagement.ID and has_server_cluster_type(ep_cluster) and + clusters.ZoneManagement.are_features_supported(clusters.ZoneManagement.types.Feature.USER_DEFINED, ep_cluster.feature_map) then + table.insert(main_component_capabilities, capabilities.zoneManagement.ID) elseif ep_cluster.cluster_id == clusters.OccupancySensing.ID and has_server_cluster_type(ep_cluster) then table.insert(main_component_capabilities, capabilities.motionSensor.ID) elseif ep_cluster.cluster_id == clusters.WebRTCTransportProvider.ID and has_server_cluster_type(ep_cluster) then diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index d443bdc573..c238bfd2ca 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -62,7 +62,8 @@ local mock_device = test.mock_device.build_test_matter_device({ { cluster_id = clusters.ZoneManagement.ID, feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE | - clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY, + clusters.ZoneManagement.types.Feature.PER_ZONE_SENSITIVITY | + clusters.ZoneManagement.types.Feature.USER_DEFINED, cluster_type = "SERVER" }, { @@ -120,6 +121,46 @@ local mock_device = test.mock_device.build_test_matter_device({ }) local mock_device_no_per_zone_sensitivity = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = CAMERA_EP, + clusters = { + { + cluster_id = clusters.CameraAvStreamManagement.ID, + feature_map = clusters.CameraAvStreamManagement.types.Feature.VIDEO, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.ZoneManagement.ID, + feature_map = clusters.ZoneManagement.types.Feature.TWO_DIMENSIONAL_CARTESIAN_ZONE | + clusters.ZoneManagement.types.Feature.USER_DEFINED, + cluster_type = "SERVER" + }, + { + cluster_id = clusters.PushAvStreamTransport.ID, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x0142, device_type_revision = 1} -- Camera + } + } + } +}) + +local mock_device_no_user_defined_zone = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("camera.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, matter_version = {hardware = 1, software = 1}, @@ -229,6 +270,26 @@ local function test_init_no_per_zone_sensitivity() mock_device_no_per_zone_sensitivity:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end +local subscribe_request_no_user_defined_zone +local subscribed_attributes_no_user_defined_zone = { + clusters.CameraAvStreamManagement.attributes.AttributeList, +} + +local function test_init_no_user_defined_zone() + test.mock_device.add_test_device(mock_device_no_user_defined_zone) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_user_defined_zone.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_user_defined_zone.id, "init" }) + subscribe_request_no_user_defined_zone = subscribed_attributes_no_user_defined_zone[1]:subscribe(mock_device_no_user_defined_zone) + subscribe_request_no_user_defined_zone:merge(cluster_base.subscribe(mock_device_no_user_defined_zone, nil, camera_fields.CameraAVSMFeatureMapAttr.cluster, camera_fields.CameraAVSMFeatureMapAttr.ID)) + subscribe_request_no_user_defined_zone:merge(cluster_base.subscribe(mock_device_no_user_defined_zone, nil, camera_fields.ZoneManagementFeatureMapAttr.cluster, camera_fields.ZoneManagementFeatureMapAttr.ID)) + for i, attr in ipairs(subscribed_attributes_no_user_defined_zone) do + if i > 1 then subscribe_request_no_user_defined_zone:merge(attr:subscribe(mock_device_no_user_defined_zone)) end + end + test.socket.matter:__expect_send({mock_device_no_user_defined_zone.id, subscribe_request_no_user_defined_zone}) + test.socket.device_lifecycle:__queue_receive({ mock_device_no_user_defined_zone.id, "doConfigure" }) + mock_device_no_user_defined_zone:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local additional_subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, clusters.CameraAvStreamManagement.attributes.ImageRotation, @@ -485,6 +546,69 @@ local function update_device_profile_no_per_zone_sensitivity() test.socket.matter:__expect_send({mock_device_no_per_zone_sensitivity.id, subscribe_request_no_per_zone_sensitivity}) end +local additional_subscribed_attributes_no_user_defined_zone = { + clusters.CameraAvStreamManagement.attributes.StatusLightBrightness, + clusters.CameraAvStreamManagement.attributes.StatusLightEnabled, + clusters.CameraAvStreamManagement.attributes.RateDistortionTradeOffPoints, + clusters.CameraAvStreamManagement.attributes.MaxEncodedPixelRate, + clusters.CameraAvStreamManagement.attributes.VideoSensorParams, + clusters.CameraAvStreamManagement.attributes.AllocatedVideoStreams, + clusters.CameraAvSettingsUserLevelManagement.attributes.DPTZStreams, + clusters.CameraAvStreamManagement.attributes.MinViewportResolution, + clusters.CameraAvStreamManagement.attributes.Viewport, + clusters.CameraAvStreamManagement.attributes.AttributeList, + clusters.OnOff.attributes.OnOff, +} + +local function update_device_profile_no_user_defined_zone() + local expected_metadata = { + optional_component_capabilities = { + { + "main", + { + "videoCapture2", + "cameraViewportSettings", + "videoStreamSettings", + } + }, + { + "statusLed", + { + "switch", + "mode" + } + } + }, + profile = "camera" + } + + test.socket.matter:__queue_receive({ + mock_device_no_user_defined_zone.id, + clusters.CameraAvStreamManagement.attributes.AttributeList:build_test_report_data(mock_device_no_user_defined_zone, CAMERA_EP, { + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightEnabled.ID), + uint32(clusters.CameraAvStreamManagement.attributes.StatusLightBrightness.ID) + }) + }) + mock_device_no_user_defined_zone:expect_metadata_update(expected_metadata) + test.wait_for_events() + local updated_device_profile = t_utils.get_profile_definition( + "camera.yml", {enabled_optional_capabilities = expected_metadata.optional_component_capabilities} + ) + test.wait_for_events() + test.socket.device_lifecycle:__queue_receive(mock_device_no_user_defined_zone:generate_info_changed({ profile = updated_device_profile })) + + test.socket.capability:__expect_send( + mock_device_no_user_defined_zone:generate_test_message("main", capabilities.videoStreamSettings.supportedFeatures( + {"liveStreaming", "clipRecording", "perStreamViewports"} + )) + ) + + for _, attr in ipairs(additional_subscribed_attributes_no_user_defined_zone) do + subscribe_request_no_user_defined_zone:merge(attr:subscribe(mock_device_no_user_defined_zone)) + end + test.socket.matter:__expect_send({mock_device_no_user_defined_zone.id, subscribe_request_no_user_defined_zone}) +end + -- Matter Handler UTs test.register_coroutine_test( @@ -3185,49 +3309,61 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Zone Management trigger reports should omit sensitivity when per-zone sensitivity is unsupported, even if provided by client", + "Camera profile should include zoneManagement when USER_DEFINED feature is present", function() update_device_profile_no_per_zone_sensitivity() test.wait_for_events() - -- Create a trigger with + end, + { + test_init = test_init_no_per_zone_sensitivity, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Camera profile should not include zoneManagement when USER_DEFINED feature is missing", + function() + update_device_profile_no_user_defined_zone() + test.wait_for_events() + end, + { + test_init = test_init_no_user_defined_zone, + min_api_version = 17 + } +) + +test.register_coroutine_test( + "Zone Management trigger command should be ignored when zoneManagement capability is not exposed", + function() + update_device_profile_no_user_defined_zone() + test.wait_for_events() test.socket.capability:__queue_receive({ - mock_device_no_per_zone_sensitivity.id, + mock_device_no_user_defined_zone.id, { capability = "zoneManagement", component = "main", command = "createOrUpdateTrigger", args = { 1, 10, 3, 15, 3, 5 }} }) - test.socket.matter:__expect_send({ - mock_device_no_per_zone_sensitivity.id, clusters.ZoneManagement.server.commands.CreateOrUpdateTrigger(mock_device_no_per_zone_sensitivity, CAMERA_EP, { - zone_id = 1, - initial_duration = 10, - augmentation_duration = 3, - max_duration = 15, - blind_duration = 3 - }) - }) + test.wait_for_events() end, { - test_init = test_init_no_per_zone_sensitivity, + test_init = test_init_no_user_defined_zone, min_api_version = 17 } ) test.register_coroutine_test( - "Zone Management setSensitivity command should handle global sensitivity when per-zone sensitivity is unsupported", + "Zone Management setSensitivity command should be ignored when zoneManagement capability is not exposed", function() - update_device_profile_no_per_zone_sensitivity() + update_device_profile_no_user_defined_zone() test.wait_for_events() test.socket.capability:__queue_receive({ - mock_device_no_per_zone_sensitivity.id, + mock_device_no_user_defined_zone.id, { capability = "zoneManagement", component = "main", command = "setSensitivity", args = { 5 } } }) - test.socket.matter:__expect_send({ - mock_device_no_per_zone_sensitivity.id, - clusters.ZoneManagement.attributes.Sensitivity:write(mock_device_no_per_zone_sensitivity, CAMERA_EP, 5) - }) + test.wait_for_events() end, { - test_init = test_init_no_per_zone_sensitivity, + test_init = test_init_no_user_defined_zone, min_api_version = 17 } ) From e566c1bcf08b71af02bdad529c23da7795e5300b Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 20 Jul 2026 16:00:55 -0500 Subject: [PATCH 062/102] Added verification to min/max-heat/coolSetpointLimit tests --- .../src/test/test_zenwithin_thermostat.lua | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua index 721853579a..c16757366d 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua @@ -523,57 +523,61 @@ test.register_coroutine_test( } ) -test.register_message_test( +test.register_coroutine_test( "MinHeatSetpointLimit report stores device field", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, Thermostat.attributes.MinHeatSetpointLimit:build_test_attr_report(mock_device, 500) } - } - }, + function () + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MinHeatSetpointLimit:build_test_attr_report(mock_device, 500) + }) + test.wait_for_events() + assert(mock_device:get_field("minHeatSetpoint") == 500, "minHeatSetpoint field not stored correctly") + end, { min_api_version = 17 } ) -test.register_message_test( +test.register_coroutine_test( "MaxHeatSetpointLimit report stores device field", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, Thermostat.attributes.MaxHeatSetpointLimit:build_test_attr_report(mock_device, 3500) } - } - }, + function () + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MaxHeatSetpointLimit:build_test_attr_report(mock_device, 3500) + }) + test.wait_for_events() + assert(mock_device:get_field("maxHeatSetpoint") == 3500, "maxHeatSetpoint field not stored correctly") + end, { min_api_version = 17 } ) -test.register_message_test( +test.register_coroutine_test( "MinCoolSetpointLimit report stores device field", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, Thermostat.attributes.MinCoolSetpointLimit:build_test_attr_report(mock_device, 1600) } - } - }, + function () + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MinCoolSetpointLimit:build_test_attr_report(mock_device, 1600) + }) + test.wait_for_events() + assert(mock_device:get_field("minCoolSetpoint") == 1600, "minCoolSetpoint field not stored correctly") + end, { min_api_version = 17 } ) -test.register_message_test( +test.register_coroutine_test( "MaxCoolSetpointLimit report stores device field", - { - { - channel = "zigbee", - direction = "receive", - message = { mock_device.id, Thermostat.attributes.MaxCoolSetpointLimit:build_test_attr_report(mock_device, 3200) } - } - }, + function () + test.socket.zigbee:__queue_receive({ + mock_device.id, + Thermostat.attributes.MaxCoolSetpointLimit:build_test_attr_report(mock_device, 3200) + }) + test.wait_for_events() + assert(mock_device:get_field("maxCoolSetpoint") == 3200, "maxCoolSetpoint field not stored correctly") + end, { min_api_version = 17 } From 60a0ce7d02e457c91fff277c592e3f4ff3444a2d Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Tue, 21 Jul 2026 12:32:52 -0500 Subject: [PATCH 063/102] 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 1d58e176f7185a4e5845cc2362b7cf1cada786c9 Mon Sep 17 00:00:00 2001 From: zs Date: Thu, 9 Jul 2026 18:59:59 +0800 Subject: [PATCH 064/102] Add step function for Tuya Zigbee curtains --- .../profiles/window-treatment-reverse.yml | 2 + .../tuya-zigbee/src/curtain/init.lua | 41 +++++ .../src/test/test_tuya_curtain.lua | 166 +++++++++++++++++- 3 files changed, 208 insertions(+), 1 deletion(-) diff --git a/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml b/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml index 9712eb6849..d0f47cdafd 100644 --- a/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml +++ b/drivers/Unofficial/tuya-zigbee/profiles/window-treatment-reverse.yml @@ -6,6 +6,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: windowShadePreset version: 1 - id: firmwareUpdate diff --git a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua index 03c34cac65..284486e88b 100644 --- a/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua +++ b/drivers/Unofficial/tuya-zigbee/src/curtain/init.lua @@ -23,6 +23,14 @@ local packet_id = 0 local PRESET_LEVEL = 50 local PRESET_LEVEL_KEY = "_presetLevel" +-- When the curtain is moving, LATEST_TARGET_LEVEL is used to store the latest target position value, +-- which will be cleared after 3 seconds timeout +local LATEST_TARGET_LEVEL = "_latestTargetLevel" +-- Field name for the timer that clears the target position after timeout +local TARGET_LEVEL_TIME_OUT = "_targetLevelTimeOut" +-- Timeout for the stateless step to accumulate +local TARGET_LEVEL_TIME_OUT_SECONDS = 15 + local FINGERPRINTS = { { mfr = "_TZE284_nladmfvf", model = "TS0601"} } @@ -153,6 +161,36 @@ local function tuya_cluster_handler(driver, device, zb_rx) end end +local function handle_step_shade_level(driver, device, command) + local step = command.args.stepSize or command.args[1] or (command.positional_args and command.positional_args.stepSize) + + -- When LATEST_TARGET_LEVEL is empty, it means the curtain motor is not moving, + -- and capabilities.windowShadeLevel.shadeLevel is used as the reference for position offset calculation; + -- when LATEST_TARGET_LEVEL is not empty, it means the curtain motor is moving, + -- and LATEST_TARGET_LEVEL is used as the reference for position offset calculation to obtain a more accurate position calculation. + local latest_target_level = device:get_field(LATEST_TARGET_LEVEL) + local current_level = latest_target_level or + device:get_latest_state("main", capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + + local target_level = utils.clamp_value(current_level + step, 0, 100) + target_level = utils.round(target_level) + -- Cancel any previous timer and set a 15 second timeout timer to ensure target_level is cleared + -- This is to prevent the stateless step from accumulating indefinitely, while ensuring that a single + -- "scroll" action can accumulate multiple steps within a short time frame + if device:get_field(TARGET_LEVEL_TIME_OUT) then + device.thread:cancel_timer(device:get_field(TARGET_LEVEL_TIME_OUT)) + end + local timer = device.thread:call_with_delay(TARGET_LEVEL_TIME_OUT_SECONDS, function(d) + device:set_field(LATEST_TARGET_LEVEL, nil) + end) + device:set_field(TARGET_LEVEL_TIME_OUT, timer) + device:set_field(LATEST_TARGET_LEVEL, target_level) + + local new_command = { args = { shadeLevel = target_level }, component = command.component } + window_shade_level(driver, device, new_command) +end + local tuya_curtain_driver = { NAME = "tuya curtain", lifecycle_handlers = { @@ -170,6 +208,9 @@ local tuya_curtain_driver = { [capabilities.windowShadeLevel.ID] = { [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = window_shade_level }, + [capabilities.statelessWindowShadeLevelStep.ID] = { + [capabilities.statelessWindowShadeLevelStep.commands.stepShadeLevel.NAME] = handle_step_shade_level + }, [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_preset, [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = set_preset_position_cmd diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua index ca09608347..1cf3335d9e 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -244,4 +244,168 @@ test.register_message_test( } } ) -test.run_registered_tests() + +test.register_message_test( + "Handle Window Shade step level command - step up", + { + { + channel = "capability", + direction = "receive", + message = { mock_simple_device.id, { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }, stepSize = 10 } } + }, + { + channel = "zigbee", + direction = "send", + -- For _TZE284_nladmfvf, level is inverted: 100 - 10 = 90 (0x5A) + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x5a', 0) } + } + } +) + +test.register_message_test( + "Handle Window Shade step level command - step down", + { + { + channel = "capability", + direction = "receive", + message = { mock_simple_device.id, { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -10 }, stepSize = -10 } } + }, + { + channel = "zigbee", + direction = "send", + -- For _TZE284_nladmfvf, level is inverted: 100 - 0 = 100 (0x64), but clamped to 0 so 100 - 0 = 100 + message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x64', 0) } + } + } +) + +test.register_coroutine_test( + "Handle Window Shade step level command - step up from current level", + function() + -- First, simulate a current shade level of 40 + test.socket.zigbee:__queue_receive({ + mock_simple_device.id, + tuya_utils.build_test_attr_report(mock_simple_device, '\x03', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x28', 0x01) + }) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(40)) + ) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShade.windowShade("partially open")) + ) + test.wait_for_events() + + -- Then send step command with stepSize 20, should result in level 60 + -- For _TZE284_nladmfvf, level is inverted: 100 - 60 = 40 (0x28) + test.socket.capability:__queue_receive({ + mock_simple_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 20 }, stepSize = 20 } + }) + test.socket.zigbee:__expect_send({ + mock_simple_device.id, + tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x28', 0) + }) + end +) + +test.register_coroutine_test( + "Handle Window Shade step level command - step up clamped to 100", + function() + -- First, simulate a current shade level of 90 + test.socket.zigbee:__queue_receive({ + mock_simple_device.id, + tuya_utils.build_test_attr_report(mock_simple_device, '\x03', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x5a', 0x01) + }) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(90)) + ) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShade.windowShade("partially open")) + ) + test.wait_for_events() + + -- Then send step command with stepSize 20, should be clamped to 100 + -- For _TZE284_nladmfvf, level is inverted: 100 - 100 = 0 (0x00) + test.socket.capability:__queue_receive({ + mock_simple_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 20 }, stepSize = 20 } + }) + test.socket.zigbee:__expect_send({ + mock_simple_device.id, + tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x00', 0) + }) + end +) + +test.register_coroutine_test( + "Handle Window Shade step level command - step down clamped to 0", + function() + -- First, simulate a current shade level of 10 + test.socket.zigbee:__queue_receive({ + mock_simple_device.id, + tuya_utils.build_test_attr_report(mock_simple_device, '\x03', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x0a', 0x01) + }) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(10)) + ) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShade.windowShade("partially open")) + ) + test.wait_for_events() + + -- Then send step command with stepSize -20, should be clamped to 0 + -- For _TZE284_nladmfvf, level is inverted: 100 - 0 = 100 (0x64) + test.socket.capability:__queue_receive({ + mock_simple_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -20 }, stepSize = -20 } + }) + test.socket.zigbee:__expect_send({ + mock_simple_device.id, + tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x64', 0) + }) + end +) + +test.register_coroutine_test( + "Handle Window Shade step level command - uses latest_target_level when available", + function() + -- First, simulate a current shade level of 40 + test.socket.zigbee:__queue_receive({ + mock_simple_device.id, + tuya_utils.build_test_attr_report(mock_simple_device, '\x03', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x28', 0x01) + }) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(40)) + ) + test.socket.capability:__expect_send( + mock_simple_device:generate_test_message("main", capabilities.windowShade.windowShade("partially open")) + ) + test.wait_for_events() + + -- Send first step command to set latest_target_level to 50 + -- For _TZE284_nladmfvf, level is inverted: 100 - 50 = 50 (0x32) + test.socket.capability:__queue_receive({ + mock_simple_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }, stepSize = 10 } + }) + test.socket.zigbee:__expect_send({ + mock_simple_device.id, + tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x32', 0) + }) + test.wait_for_events() + + -- Send second step command before timeout - should use latest_target_level (50) as base + -- Target = 50 + 10 = 60, For _TZE284_nladmfvf: 100 - 60 = 40 (0x28) + -- Packet ID is 1 because it was incremented after the first step command + test.socket.capability:__queue_receive({ + mock_simple_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }, stepSize = 10 } + }) + test.socket.zigbee:__expect_send({ + mock_simple_device.id, + tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x28', 1) + }) + end +) + +test.run_registered_tests() \ No newline at end of file From 0b239eb6aad13a83146cc90ace1f10ba333e7309 Mon Sep 17 00:00:00 2001 From: Hong Ye/MDE Lab /SRC-Nanjing/Staff Engineer/Samsung Electronics Date: Thu, 25 Jun 2026 20:46:45 +0800 Subject: [PATCH 065/102] Update matter blind edge driver Signed-off-by: Hong Ye/MDE Lab /SRC-Nanjing/Staff Engineer/Samsung Electronics --- .../profiles/window-covering-battery.yml | 2 + .../profiles/window-covering-batteryLevel.yml | 2 + .../profiles/window-covering-profile.yml | 2 + .../profiles/window-covering-tilt-battery.yml | 2 + .../profiles/window-covering-tilt.yml | 2 + .../profiles/window-covering.yml | 2 + .../matter-window-covering/src/init.lua | 70 +++++- .../src/test/test_matter_window_covering.lua | 228 ++++++++++++++++++ 8 files changed, 307 insertions(+), 3 deletions(-) diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml index a1896ca634..ad2dc6f0ac 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-battery.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: battery version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml index fa46d87cc6..c541d47ae8 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-batteryLevel.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: batteryLevel version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml index 903565e68c..94e98998b3 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-profile.yml @@ -9,6 +9,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: battery version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml index 27f7f1f61a..bca45d37af 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt-battery.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: windowShadeTiltLevel version: 1 - id: battery diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml index c6a759b610..99a579e2c6 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering-tilt.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: windowShadeTiltLevel version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml b/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml index fbd40ed08d..e90a3fdb23 100644 --- a/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml +++ b/drivers/SmartThings/matter-window-covering/profiles/window-covering.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index 66581d0ae8..bfde1e3fc8 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -9,6 +9,7 @@ local im = require "st.matter.interaction_model" local log = require "log" local clusters = require "st.matter.clusters" local MatterDriver = require "st.matter.driver" +local utils = require "st.utils" local CURRENT_LIFT = "__current_lift" local CURRENT_TILT = "__current_tilt" @@ -20,6 +21,11 @@ local battery_support = { local REVERSE_POLARITY = "__reverse_polarity" local PRESET_LEVEL_KEY = "__preset_level_key" local DEFAULT_PRESET_LEVEL = 50 +local LATEST_TARGET_LEVEL = "_latest_target_level" +local TARGET_LEVEL_TIME_OUT = "_target_level_timeout" +local TARGET_LEVEL_TIME_OUT_SECONDS = 15 + +local TARGET_REACH_TOLERANCE = 1 local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -175,15 +181,44 @@ local function handle_shade_level(driver, device, cmd) device:send(req) end +local function handle_step_shade_level(driver, device, cmd) + local step = cmd.args.stepSize + + local current_level = device:get_field(LATEST_TARGET_LEVEL) or + device:get_latest_state("main", capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME) or 0 + + local target_level = utils.clamp_value(current_level + step, 0, 100) + + device:set_field(LATEST_TARGET_LEVEL, target_level) + + local old_timer = device:get_field(TARGET_LEVEL_TIME_OUT) + if old_timer ~= nil then + device.thread:cancel_timer(old_timer) + end + + -- Set 15 second timeout timer to ensure target_level is cleared + local timer = device.thread:call_with_delay(TARGET_LEVEL_TIME_OUT_SECONDS, function(d) + device:set_field(LATEST_TARGET_LEVEL, nil) + end) + device:set_field(TARGET_LEVEL_TIME_OUT, timer) + + driver:inject_capability_command(device, { + capability = capabilities.windowShadeLevel.ID, + component = cmd.component, + command = capabilities.windowShadeLevel.commands.setShadeLevel.NAME, + named_args = { shadeLevel = target_level } + }) +end + -- move to shade tilt level between 0-100 local function handle_shade_tilt_level(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) local tilt_percentage_value = 100 - cmd.args.level local hundredths_tilt_percentage = tilt_percentage_value * 100 - local req = clusters.WindowCovering.server.commands.GoToTiltPercentage( + device:send(clusters.WindowCovering.server.commands.GoToTiltPercentage( device, endpoint_id, hundredths_tilt_percentage - ) - device:send(req) + )) end -- current lift/tilt percentage, changed to 100ths percent @@ -194,6 +229,22 @@ local current_pos_handler = function(attribute) end local windowShade = capabilities.windowShade.windowShade local position = 100 - math.floor(ib.data.value / 100) + + -- Step control logic + local target_level_field = device:get_field(LATEST_TARGET_LEVEL) + if target_level_field and attribute == capabilities.windowShadeLevel.shadeLevel then + -- Allow ±1 degree tolerance for reaching target + if math.abs(position - target_level_field) <= TARGET_REACH_TOLERANCE then + -- Device reached target position, clear target marker and timeout timer + device:set_field(LATEST_TARGET_LEVEL, nil) + local timer = device:get_field(TARGET_LEVEL_TIME_OUT) + if timer ~= nil then + device.thread:cancel_timer(timer) + device:set_field(TARGET_LEVEL_TIME_OUT, nil) + end + end + end + local reverse = device:get_field(REVERSE_POLARITY) device:emit_event_for_endpoint(ib.endpoint_id, attribute(position)) @@ -253,6 +304,16 @@ local function current_status_handler(driver, device, ib, response) device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closing() or windowShade.opening()) elseif state == 2 then -- closing device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.opening() or windowShade.closing()) + elseif state == 0 then -- stopped (idle) + -- Clear target_level when device stops moving + local target_level_field = device:get_field(LATEST_TARGET_LEVEL) + if target_level_field then + device:set_field(LATEST_TARGET_LEVEL, nil) + local timer = device:get_field(TARGET_LEVEL_TIME_OUT) + if timer ~= nil then + device.thread:cancel_timer(timer) + end + end elseif state ~= 0 then -- unknown device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) end @@ -354,6 +415,9 @@ local matter_driver_template = { [capabilities.windowShadeLevel.ID] = { [capabilities.windowShadeLevel.commands.setShadeLevel.NAME] = handle_shade_level, }, + [capabilities.statelessWindowShadeLevelStep.ID] = { + [capabilities.statelessWindowShadeLevelStep.commands.stepShadeLevel.NAME] = handle_step_shade_level + }, [capabilities.windowShadeTiltLevel.ID] = { [capabilities.windowShadeTiltLevel.commands.setShadeTiltLevel.NAME] = handle_shade_tilt_level, }, diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index 9f273037f3..ae91d7c9cd 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -1222,4 +1222,232 @@ test.register_coroutine_test( } ) +-- stepShadeLevel tests + +test.register_coroutine_test( + "WindowShade stepShadeLevel cmd handler - step up", function() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 9000)} + ) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel cmd handler - step down", function() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -20 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 7000)} + ) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel cmd handler - continuous step with target tracking", function() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 7000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(30) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 6000)} + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 5000)} + ) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel - target reached clears target marker", function() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 4000)} + ) + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 4000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(60) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel - step up to maximum (100)", function() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 500 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(95) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 0)} + ) + end, + { + min_api_version = 19 + } +) + +test.register_coroutine_test( + "WindowShade stepShadeLevel - step down to minimum (0)", function() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 9500 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(5) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.partially_open() + ) + ) + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -10 }}, + } + ) + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 10000)} + ) + end, + { + min_api_version = 19 + } +) + test.run_registered_tests() From 25c8bb36f574e3e5a9e6aff6939a9318882cf254 Mon Sep 17 00:00:00 2001 From: zhangshen001 Date: Wed, 22 Jul 2026 23:51:42 +0800 Subject: [PATCH 066/102] Zigbee Window Treatment: Add step shade level functionality to blinds (#3108) * Add step shade level functionality to Zigbee curtains using Common Handler. --------- Co-authored-by: Harrison Carter --- ...ndow-treatment-aqara-curtain-driver-e1.yml | 2 + ...ow-treatment-aqara-roller-shade-rotate.yml | 2 + .../profiles/window-treatment-aqara.yml | 2 + .../profiles/window-treatment-battery.yml | 2 + .../profiles/window-treatment-no-preset.yml | 2 + .../profiles/window-treatment-powerSource.yml | 2 + ...w-treatment-profile-no-firmware-update.yml | 2 + .../profiles/window-treatment-profile.yml | 2 + .../profiles/window-treatment-reverse.yml | 2 + .../zigbee-window-treatment/src/init.lua | 6 +- .../src/test/test_zigbee_window_treatment.lua | 255 +++++++++++ ..._zigbee_window_treatment_inverted_step.lua | 346 +++++++++++++++ ...ee_window_treatment_level_cluster_step.lua | 400 ++++++++++++++++++ .../src/window_shade_utils.lua | 40 ++ 14 files changed, 1064 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_inverted_step.lua create mode 100644 drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_level_cluster_step.lua diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara-curtain-driver-e1.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara-curtain-driver-e1.yml index dbd219e523..05e3fe0085 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara-curtain-driver-e1.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara-curtain-driver-e1.yml @@ -6,6 +6,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: stse.initializedStateWithGuide version: 1 - id: stse.hookLockState diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara-roller-shade-rotate.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara-roller-shade-rotate.yml index c454c4d590..e7f5e8d8af 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara-roller-shade-rotate.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara-roller-shade-rotate.yml @@ -6,6 +6,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: stse.initializedStateWithGuide version: 1 - id: stse.shadeRotateState diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara.yml index 6c2cc967e7..4dc959ef93 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-aqara.yml @@ -6,6 +6,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: stse.deviceInitialization version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml index be0ae73d76..e9b6178ff8 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-battery.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: battery version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-no-preset.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-no-preset.yml index 7a65434c46..67ba3c186d 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-no-preset.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-no-preset.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml index 7323ee0218..d72dc33308 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-powerSource.yml @@ -6,6 +6,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: windowShadePreset version: 1 - id: powerSource diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile-no-firmware-update.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile-no-firmware-update.yml index 8e641e6b42..dfeeed7aa1 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile-no-firmware-update.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile-no-firmware-update.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: refresh version: 1 categories: diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml index b10a5ea101..c9fce872c0 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-profile.yml @@ -8,6 +8,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: firmwareUpdate version: 1 - id: refresh diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml index 0eb666a5d2..54b4312210 100644 --- a/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/window-treatment-reverse.yml @@ -6,6 +6,8 @@ components: version: 1 - id: windowShadeLevel version: 1 + - id: statelessWindowShadeLevelStep + version: 1 - id: windowShadePreset version: 1 - id: firmwareUpdate diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index 0a093645f8..b58c20a5be 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -33,6 +33,7 @@ local zigbee_window_treatment_driver_template = { capabilities.windowShade, capabilities.windowShadePreset, capabilities.windowShadeLevel, + capabilities.statelessWindowShadeLevelStep, capabilities.powerSource, capabilities.battery }, @@ -40,7 +41,10 @@ local zigbee_window_treatment_driver_template = { [capabilities.windowShadePreset.ID] = { [capabilities.windowShadePreset.commands.setPresetPosition.NAME] = window_shade_utils.set_preset_position_cmd, [capabilities.windowShadePreset.commands.presetPosition.NAME] = window_shade_utils.window_shade_preset_cmd, - } + }, + [capabilities.statelessWindowShadeLevelStep.ID] = { + [capabilities.statelessWindowShadeLevelStep.commands.stepShadeLevel.NAME] = window_shade_utils.step_shade_level_handler + }, }, lifecycle_handlers = { init = init_handler, diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua index ed01a87249..145876ff64 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua @@ -337,4 +337,259 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "statelessWindowShadeLevelStep stepShadeLevel - positive step", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 50) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + mock_device:set_field("_latestTargetLevel", 50) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 60) + }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "statelessWindowShadeLevelStep stepShadeLevel - negative step", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 30) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 30 } } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + mock_device:set_field("_latestTargetLevel", 30) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -20 } } + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10) + }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "statelessWindowShadeLevelStep stepShadeLevel - accumulated steps with mixed directions", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 50) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + mock_device:set_field("_latestTargetLevel", 50) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 60) + }) + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -5 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 55) + }) + test.wait_for_events() + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 15 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 70) + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "statelessWindowShadeLevelStep stepShadeLevel - clamped to 100", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 90) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 90 } } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + mock_device:set_field("_latestTargetLevel", 90) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 50 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 100) + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "statelessWindowShadeLevelStep stepShadeLevel - clamped to 0", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 10) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 10 } } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + mock_device:set_field("_latestTargetLevel", 10) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -50 } } + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + clusters.WindowCovering.server.commands.GoToLiftPercentage(mock_device, 0) + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "statelessWindowShadeLevelStep stepShadeLevel - zero stepSize", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 50) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + mock_device:set_field("_latestTargetLevel", 50) + + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 0 } } + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + +-- Test step_shade_level_handler with zero step +test.register_coroutine_test( + "step_shade_level_handler returns early when stepSize is zero", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 50) + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + test.socket.capability:__expect_send({ + mock_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + mock_device:set_field("_latestTargetLevel", 50) + + -- Send zero step - should return early without sending any command + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 0 } } + }) + test.wait_for_events() + end, + { + min_api_version = 17 + } +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_inverted_step.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_inverted_step.lua new file mode 100644 index 0000000000..3c7c0cbee2 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_inverted_step.lua @@ -0,0 +1,346 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Test for statelessWindowShadeLevelStep capability with Somfy (reverse) devices +-- Tests devices like Somfy Glydea that use inverted lift percentage + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local WindowCovering = clusters.WindowCovering + +-- Create mock Somfy Glydea device using WindowCovering cluster with inverted logic +-- This device matches the somfy sub-driver which inverts the lift percentage +local somfy_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("window-treatment-profile.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SOMFY", + model = "Glydea Ultra Curtain", + server_clusters = { WindowCovering.ID } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(somfy_device) + test.socket.capability:__expect_send( + somfy_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + somfy_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end + +test.set_test_init_function(test_init) + +-- Test 1: Boundary condition - stepSize = 0 should not send command +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Somfy (reverse) - stepSize 0 does not send command", + function() + -- Set initial state via WindowCovering cluster report: 50% (inverted: device reports 50 for 50% shade) + test.socket.zigbee:__queue_receive({ + somfy_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(somfy_device, 50) + }) + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + -- Somfy sub-driver emits "opening" for initial report when no previous state exists + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + somfy_device:set_field("_latestTargetLevel", 50) + + -- Send stepShadeLevel command with stepSize = 0 + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 0 } } + }) + + -- Should NOT send any Zigbee command (stepSize 0 is ignored) + test.wait_for_events() + end, + { min_api_version = 17 } +) + +-- Test 2: Normal step up operation with Somfy (reverse) device +-- Somfy inverts the value: shadeLevel 60 means device goes to 100-60=40 +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Somfy (reverse) - normal step up", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: WindowCovering cluster reports 50 (inverted: 100-50=50 shade level) + test.socket.zigbee:__queue_receive({ + somfy_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(somfy_device, 50) + }) + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + -- Somfy sub-driver emits "opening" for initial report when no previous state exists + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + somfy_device:set_field("_latestTargetLevel", 50) + + -- Send stepShadeLevel command with stepSize = 10 + -- new_target_level = clamp(50 + 10, 0, 100) = 60 + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } + }) + + -- Note: Somfy sub-driver's window_shade_level_cmd directly sends Zigbee command without emitting events + -- Somfy sub-driver's window_shade_level_cmd inverts the value: 100 - 60 = 40 + test.socket.zigbee:__expect_send({ + somfy_device.id, + WindowCovering.server.commands.GoToLiftPercentage(somfy_device, 40) + }) + end, + { min_api_version = 17 } +) + +-- Test 3: Normal step down operation with Somfy (reverse) device +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Somfy (reverse) - normal step down", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: WindowCovering cluster reports 25 (inverted: 100-25=75 shade level) + test.socket.zigbee:__queue_receive({ + somfy_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(somfy_device, 25) + }) + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 75 } } + }) + -- Somfy sub-driver emits "opening" for initial report when no previous state exists + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + somfy_device:set_field("_latestTargetLevel", 75) + + -- Send stepShadeLevel command with stepSize = -25 + -- new_target_level = clamp(75 + (-25), 0, 100) = 50 + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -25 } } + }) + + -- Note: Somfy sub-driver's window_shade_level_cmd directly sends Zigbee command without emitting events + -- Somfy sub-driver's window_shade_level_cmd inverts the value: 100 - 50 = 50 + test.socket.zigbee:__expect_send({ + somfy_device.id, + WindowCovering.server.commands.GoToLiftPercentage(somfy_device, 50) + }) + end, + { min_api_version = 17 } +) + +-- Test 4: Boundary condition - value clamped to 100 (maximum) +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Somfy (reverse) - value clamped to 100", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: WindowCovering cluster reports 10 (inverted: 100-10=90 shade level) + test.socket.zigbee:__queue_receive({ + somfy_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(somfy_device, 10) + }) + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 90 } } + }) + -- Somfy sub-driver emits "opening" for initial report when no previous state exists + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + somfy_device:set_field("_latestTargetLevel", 90) + + -- Send stepShadeLevel command with stepSize = 50 + -- new_target_level = clamp(90 + 50, 0, 100) = 100 + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 50 } } + }) + + -- Note: Somfy sub-driver's window_shade_level_cmd directly sends Zigbee command without emitting events + -- Somfy sub-driver's window_shade_level_cmd inverts the value: 100 - 100 = 0 + test.socket.zigbee:__expect_send({ + somfy_device.id, + WindowCovering.server.commands.GoToLiftPercentage(somfy_device, 0) + }) + end, + { min_api_version = 17 } +) + +-- Test 5: Boundary condition - value clamped to 0 (minimum) +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Somfy (reverse) - value clamped to 0", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: WindowCovering cluster reports 80 (inverted: 100-80=20 shade level) + test.socket.zigbee:__queue_receive({ + somfy_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(somfy_device, 80) + }) + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 20 } } + }) + -- Somfy sub-driver emits "opening" for initial report when no previous state exists + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + somfy_device:set_field("_latestTargetLevel", 20) + + -- Send stepShadeLevel command with stepSize = -50 + -- new_target_level = clamp(20 + (-50), 0, 100) = 0 + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -50 } } + }) + + -- Note: Somfy sub-driver's window_shade_level_cmd directly sends Zigbee command without emitting events + -- Somfy sub-driver's window_shade_level_cmd inverts the value: 100 - 0 = 100 + test.socket.zigbee:__expect_send({ + somfy_device.id, + WindowCovering.server.commands.GoToLiftPercentage(somfy_device, 100) + }) + end, + { min_api_version = 17 } +) + +-- Test 6: Continuous step operations with mixed directions +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Somfy (reverse) - continuous steps with mixed directions", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: WindowCovering cluster reports 50 (inverted: 100-50=50 shade level) + test.socket.zigbee:__queue_receive({ + somfy_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(somfy_device, 50) + }) + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + -- Somfy sub-driver emits "opening" for initial report when no previous state exists + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + somfy_device:set_field("_latestTargetLevel", 50) + + -- Step 1: stepSize = 10 (up) -> 60% + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } + }) + -- Note: Somfy sub-driver's window_shade_level_cmd directly sends Zigbee command without emitting events + -- Somfy sub-driver's window_shade_level_cmd inverts the value: 100 - 60 = 40 + test.socket.zigbee:__expect_send({ + somfy_device.id, + WindowCovering.server.commands.GoToLiftPercentage(somfy_device, 40) + }) + test.wait_for_events() + + -- Step 2: stepSize = -5 (down) -> 55% + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -5 } } + }) + -- Note: Somfy sub-driver's window_shade_level_cmd directly sends Zigbee command without emitting events + -- Somfy sub-driver's window_shade_level_cmd inverts the value: 100 - 55 = 45 + test.socket.zigbee:__expect_send({ + somfy_device.id, + WindowCovering.server.commands.GoToLiftPercentage(somfy_device, 45) + }) + test.wait_for_events() + + -- Step 3: stepSize = 20 (up) -> 75% + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 20 } } + }) + -- Note: Somfy sub-driver's window_shade_level_cmd directly sends Zigbee command without emitting events + -- Somfy sub-driver's window_shade_level_cmd inverts the value: 100 - 75 = 25 + test.socket.zigbee:__expect_send({ + somfy_device.id, + WindowCovering.server.commands.GoToLiftPercentage(somfy_device, 25) + }) + test.wait_for_events() + end, + { min_api_version = 17 } +) + +-- Test 7: nil stepSize should not send command +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Somfy (reverse) - nil stepSize does not send command", + function() + -- Set initial state: WindowCovering cluster reports 50 (inverted: 100-50=50 shade level) + test.socket.zigbee:__queue_receive({ + somfy_device.id, + WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(somfy_device, 50) + }) + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + -- Somfy sub-driver emits "opening" for initial report when no previous state exists + test.socket.capability:__expect_send({ + somfy_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + somfy_device:set_field("_latestTargetLevel", 50) + + -- Send stepShadeLevel command with nil stepSize + test.socket.capability:__queue_receive({ + somfy_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = {} } + }) + + -- Should NOT send any Zigbee command + test.wait_for_events() + end, + { min_api_version = 17 } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_level_cluster_step.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_level_cluster_step.lua new file mode 100644 index 0000000000..602aa34bd4 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_level_cluster_step.lua @@ -0,0 +1,400 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +-- Test for statelessWindowShadeLevelStep capability with Level cluster devices +-- Tests devices like AXIS Gear that use Level cluster for window covering control + +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local t_utils = require "integration_test.utils" +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local Level = clusters.Level +local WindowCovering = clusters.WindowCovering + +-- Create mock AXIS Gear device using Level cluster +-- This device matches the axis sub-driver which uses Level cluster +local axis_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("window-treatment-profile.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "AXIS", + model = "Gear", + server_clusters = { Level.ID, WindowCovering.ID } + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(axis_device) + test.socket.capability:__expect_send( + axis_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) + ) + test.socket.capability:__expect_send( + axis_device:generate_test_message("main", capabilities.windowShadePreset.position(50, {visibility = {displayed=false}})) + ) +end + +test.set_test_init_function(test_init) + +-- Test 1: Boundary condition - stepSize = 0 should not send command +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Level cluster - stepSize 0 does not send command", + function() + -- Set initial state via Level cluster report: 50% (127 out of 254) + test.socket.zigbee:__queue_receive({ + axis_device.id, + Level.attributes.CurrentLevel:build_test_attr_report(axis_device, 127) + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + axis_device:set_field("_latestTargetLevel", 50) + + -- Send stepShadeLevel command with stepSize = 0 + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 0 } } + }) + + -- Should NOT send any Zigbee command (stepSize 0 is ignored) + test.wait_for_events() + end, + { min_api_version = 17 } +) + +-- Test 2: Normal step up operation with Level cluster +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Level cluster - normal step up", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: Level cluster reports 50% (127 out of 254) + test.socket.zigbee:__queue_receive({ + axis_device.id, + Level.attributes.CurrentLevel:build_test_attr_report(axis_device, 127) + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + axis_device:set_field("_latestTargetLevel", 50) + + -- Send stepShadeLevel command with stepSize = 10 + -- new_target_level = clamp(50 + 10, 0, 100) = 60 + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } + }) + + -- AXIS sub-driver emits shadeLevel event when handling setShadeLevel (injected by main driver) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 60 } } + }) + -- AXIS sub-driver also emits windowShade event (opening because 60 > 50) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + + -- Should send MoveToLevelWithOnOff with level 60 (152 out of 254, rounded from 152.4) + test.socket.zigbee:__expect_send({ + axis_device.id, + Level.server.commands.MoveToLevelWithOnOff(axis_device, 152) + }) + end, + { min_api_version = 17 } +) + +-- Test 3: Normal step down operation with Level cluster +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Level cluster - normal step down", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: Level cluster reports 75% (191 out of 254) + test.socket.zigbee:__queue_receive({ + axis_device.id, + Level.attributes.CurrentLevel:build_test_attr_report(axis_device, 191) + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 75 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + axis_device:set_field("_latestTargetLevel", 75) + + -- Send stepShadeLevel command with stepSize = -25 + -- new_target_level = clamp(75 + (-25), 0, 100) = 50 + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -25 } } + }) + + -- AXIS sub-driver emits shadeLevel event when handling setShadeLevel (injected by main driver) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + -- AXIS sub-driver also emits windowShade event (closing because 50 < 75) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "closing" } } + }) + + -- Should send MoveToLevelWithOnOff with level 50 (127 out of 254) + test.socket.zigbee:__expect_send({ + axis_device.id, + Level.server.commands.MoveToLevelWithOnOff(axis_device, 127) + }) + end, + { min_api_version = 17 } +) + +-- Test 4: Boundary condition - value clamped to 100 (maximum) +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Level cluster - value clamped to 100", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: Level cluster reports 90% (229 out of 254) + test.socket.zigbee:__queue_receive({ + axis_device.id, + Level.attributes.CurrentLevel:build_test_attr_report(axis_device, 229) + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 90 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + axis_device:set_field("_latestTargetLevel", 90) + + -- Send stepShadeLevel command with stepSize = 50 + -- new_target_level = clamp(90 + 50, 0, 100) = 100 + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 50 } } + }) + + -- AXIS sub-driver emits shadeLevel event when handling setShadeLevel (injected by main driver) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 100 } } + }) + -- AXIS sub-driver also emits windowShade event (opening because 100 > 90) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + + -- Should send MoveToLevelWithOnOff with level 100 (254 out of 254) + test.socket.zigbee:__expect_send({ + axis_device.id, + Level.server.commands.MoveToLevelWithOnOff(axis_device, 254) + }) + end, + { min_api_version = 17 } +) + +-- Test 5: Boundary condition - value clamped to 0 (minimum) +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Level cluster - value clamped to 0", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: Level cluster reports 20% (51 out of 254) + test.socket.zigbee:__queue_receive({ + axis_device.id, + Level.attributes.CurrentLevel:build_test_attr_report(axis_device, 51) + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 20 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + axis_device:set_field("_latestTargetLevel", 20) + + -- Send stepShadeLevel command with stepSize = -50 + -- new_target_level = clamp(20 + (-50), 0, 100) = 0 + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -50 } } + }) + + -- AXIS sub-driver emits shadeLevel event when handling setShadeLevel (injected by main driver) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 0 } } + }) + -- AXIS sub-driver also emits windowShade event (closing because 0 < 20) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "closing" } } + }) + + -- Should send MoveToLevelWithOnOff with level 0 (0 out of 254) + test.socket.zigbee:__expect_send({ + axis_device.id, + Level.server.commands.MoveToLevelWithOnOff(axis_device, 0) + }) + end, + { min_api_version = 17 } +) + +-- Test 6: Continuous step operations with mixed directions +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Level cluster - continuous steps with mixed directions", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + -- Set initial state: Level cluster reports 50% (127 out of 254) + test.socket.zigbee:__queue_receive({ + axis_device.id, + Level.attributes.CurrentLevel:build_test_attr_report(axis_device, 127) + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + axis_device:set_field("_latestTargetLevel", 50) + + -- Step 1: stepSize = 10 (up) -> 60% + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 10 } } + }) + -- AXIS sub-driver emits shadeLevel and windowShade events + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 60 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.socket.zigbee:__expect_send({ + axis_device.id, + Level.server.commands.MoveToLevelWithOnOff(axis_device, 152) + }) + test.wait_for_events() + + -- Step 2: stepSize = -5 (down) -> 55% + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { -5 } } + }) + -- AXIS sub-driver emits shadeLevel and windowShade events + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 55 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "closing" } } + }) + test.socket.zigbee:__expect_send({ + axis_device.id, + Level.server.commands.MoveToLevelWithOnOff(axis_device, 140) + }) + test.wait_for_events() + + -- Step 3: stepSize = 20 (up) -> 75% + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = { 20 } } + }) + -- AXIS sub-driver emits shadeLevel and windowShade events + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 75 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "opening" } } + }) + test.socket.zigbee:__expect_send({ + axis_device.id, + Level.server.commands.MoveToLevelWithOnOff(axis_device, 191) + }) + test.wait_for_events() + end, + { min_api_version = 17 } +) + +-- Test 7: nil stepSize should not send command +test.register_coroutine_test( + "statelessWindowShadeLevelStep with Level cluster - nil stepSize does not send command", + function() + -- Set initial state: Level cluster reports 50% (127 out of 254) + test.socket.zigbee:__queue_receive({ + axis_device.id, + Level.attributes.CurrentLevel:build_test_attr_report(axis_device, 127) + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShadeLevel", component_id = "main", attribute_id = "shadeLevel", state = { value = 50 } } + }) + test.socket.capability:__expect_send({ + axis_device.id, + { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } } + }) + test.wait_for_events() + + -- Manually set LATEST_TARGET_LEVEL to simulate the reported value + axis_device:set_field("_latestTargetLevel", 50) + + -- Send stepShadeLevel command with nil stepSize + test.socket.capability:__queue_receive({ + axis_device.id, + { capability = "statelessWindowShadeLevelStep", component = "main", command = "stepShadeLevel", args = {} } + }) + + -- Should NOT send any Zigbee command + test.wait_for_events() + end, + { min_api_version = 17 } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua index f3e09c20a6..fd07648021 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/window_shade_utils.lua @@ -1,6 +1,7 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local st_utils = require "st.utils" local capabilities = require "st.capabilities" local zcl_clusters = require "st.zigbee.zcl.clusters" @@ -9,6 +10,15 @@ local utils = {} utils.PRESET_LEVEL = 50 utils.PRESET_LEVEL_KEY = "_presetLevel" +-- LATEST_TARGET_LEVEL stores the latest target position during stateless step operations. +-- It is cleared after TARGET_LEVEL_TIME_OUT_SECONDS timeout to allow accumulated steps +-- within a short time frame while preventing indefinite accumulation. +local LATEST_TARGET_LEVEL = "_latestTargetLevel" +-- Timeout in seconds for the stateless step accumulation window +local TARGET_LEVEL_TIME_OUT_SECONDS = 15 +-- Field name for the timer that clears LATEST_TARGET_LEVEL after timeout +local TARGET_LEVEL_TIME_OUT = "_targetLevelTimeOut" + utils.get_preset_level = function(device, component) local level = device:get_latest_state(component, "windowShadePreset", "position") or device:get_field(utils.PRESET_LEVEL_KEY) or @@ -31,4 +41,34 @@ utils.set_preset_position_cmd = function(driver, device, command) device:set_field(utils.PRESET_LEVEL_KEY, command.args.position, {persist = true}) end +-- Step shade level handler for statelessWindowShadeLevelStep capability +utils.step_shade_level_handler = function(driver, device, command) + local step = command.args.stepSize or 0 + if step == 0 then return end + + -- Step from the latest target level if it exists, or from the current shade level + local latest_target_level = device:get_field(LATEST_TARGET_LEVEL) or + device:get_latest_state("main", capabilities.windowShadeLevel.ID, capabilities.windowShadeLevel.shadeLevel.NAME, 0) + local new_target_level = st_utils.clamp_value(latest_target_level + step, 0, 100) + + -- Cancel any previous timer and set a 15 second timeout timer to ensure LATEST_TARGET_LEVEL is cleared + -- This is to prevent the stateless step from accumulating indefinitely, while ensuring that a single + -- "scroll" action can accumulate multiple steps within a short time frame + if device:get_field(TARGET_LEVEL_TIME_OUT) then + device.thread:cancel_timer(device:get_field(TARGET_LEVEL_TIME_OUT)) + end + local timer = device.thread:call_with_delay(TARGET_LEVEL_TIME_OUT_SECONDS, function(d) + device:set_field(LATEST_TARGET_LEVEL, nil) + end) + device:set_field(TARGET_LEVEL_TIME_OUT, timer) + device:set_field(LATEST_TARGET_LEVEL, new_target_level) + + driver:inject_capability_command(device, { + capability = capabilities.windowShadeLevel.ID, + component = command.component, + command = capabilities.windowShadeLevel.commands.setShadeLevel.NAME, + named_args = { shadeLevel = new_target_level } + }) +end + return utils From f7de112d3dae67e29a23975a23abead307b0cfe8 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 067/102] 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 From 37dead4f3d1b1f9c34372fd000a70f9da2766a21 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:05:05 -0500 Subject: [PATCH 068/102] GHA: Fix publish test results for multiple drivers (#3143) --- .github/workflows/publish-test-results.yml | 61 +++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml index 57c8812913..099ac90648 100644 --- a/.github/workflows/publish-test-results.yml +++ b/.github/workflows/publish-test-results.yml @@ -22,9 +22,66 @@ jobs: event_file: event-file/event.json event_name: ${{ github.event.workflow_run.event }} files: "tests/*.xml" - - name: Publish coverage results + + # 5monkeys/cobertura-action has a bug where the `files` array that builds + # each per-XML markdown table is declared once outside the loop over reports + # and is never reset. When multiple XML files are passed via a glob, every + # subsequent driver's table accumulates all files from earlier drivers. + # + # Fix: determine which XML files exist at runtime, then fan out into a matrix + # job so each driver's XML is processed by its own isolated action invocation. + list-coverage-files: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.build-matrix.outputs.matrix }} + steps: + - name: Download coverage artifact + continue-on-error: true + uses: dawidd6/action-download-artifact@v6 + with: + workflow: run-tests.yml + run_id: ${{ github.event.workflow_run.id }} + name: coverage + path: coverage + - name: Build matrix of coverage files + id: build-matrix + run: | + files=$(find coverage -maxdepth 1 -name '*_coverage.xml' -printf '%f\n' 2>/dev/null | sort) + if [ -z "$files" ]; then + echo 'matrix=[]' >> $GITHUB_OUTPUT + else + json=$(echo "$files" | python3 -c 'import sys,json; print(json.dumps([l.strip() for l in sys.stdin if l.strip()]))') + echo "matrix=$json" >> $GITHUB_OUTPUT + fi + + publish-coverage: + needs: list-coverage-files + if: needs.list-coverage-files.outputs.matrix != '[]' && needs.list-coverage-files.outputs.matrix != '' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + coverage_file: ${{ fromJson(needs.list-coverage-files.outputs.matrix) }} + steps: + - name: Download coverage artifact + uses: dawidd6/action-download-artifact@v6 + with: + workflow: run-tests.yml + run_id: ${{ github.event.workflow_run.id }} + name: coverage + path: coverage + - name: Download pr_number artifact + uses: dawidd6/action-download-artifact@v6 + with: + workflow: run-tests.yml + run_id: ${{ github.event.workflow_run.id }} + name: pr_number + path: pr_number + - run: echo "pr_number=$(cat pr_number/pr_number.txt)" >> $GITHUB_ENV + - name: Publish coverage for ${{ matrix.coverage_file }} uses: 5monkeys/cobertura-action@master with: pull_request_number: ${{ env.pr_number }} - path: "coverage/*.xml" + path: coverage/${{ matrix.coverage_file }} minimum_coverage: 90 + report_name: ${{ matrix.coverage_file }} From dad39acf14e84f71c3caba4fdb45483a69f222f7 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:23:11 -0500 Subject: [PATCH 069/102] Luacov Config: Update Exclude Pathing for Embedded Clusters (#3141) --- tools/config.luacov | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/config.luacov b/tools/config.luacov index b3edf6673b..26034bfb27 100644 --- a/tools/config.luacov +++ b/tools/config.luacov @@ -3,6 +3,8 @@ configuration = { "scripting-engine", "lua_libs", "test", + "embedded_clusters", + "embedded_cluster_utils", "unofficial", "DishwasherAlarm", "DishwasherMode", From 9adc33ddff02cfd81b4df57e2510227134b2b4a8 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:24:42 -0500 Subject: [PATCH 070/102] GHA: Fix driver test run warnings (#3139) --- tools/run_driver_tests_p.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/run_driver_tests_p.py b/tools/run_driver_tests_p.py index 16180f7682..20f22938e9 100644 --- a/tools/run_driver_tests_p.py +++ b/tools/run_driver_tests_p.py @@ -29,8 +29,8 @@ def per_driver_task(driver_dir): else: failure_output = None if driver_dir.name in CHANGED_DRIVERS: - with driver_dir.parent.parent.parent.joinpath("tools/coverage_output").joinpath(driver_dir.name+"_coverage.xml") as outfile: - subprocess.run("luacov-cobertura -o {} -c {}".format(outfile, LUACOV_CONFIG), shell=True) + outfile = driver_dir.parent.parent.parent.joinpath("tools/coverage_output").joinpath(driver_dir.name+"_coverage.xml") + subprocess.run("luacov-cobertura -o {} -c {}".format(outfile, LUACOV_CONFIG), shell=True) return failure_output def run_test(test_file): From 40f6c19d2525d07418625310c5e2ac76463efde7 Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 13 Jul 2026 12:59:12 -0500 Subject: [PATCH 071/102] Matter Sensor: Handle invalid sensor unit conversions, add more conversions --- .../attribute_handlers.lua | 18 +++--- .../air_quality_sensor_utils/fields.lua | 61 ++++++++++++++---- .../air_quality_sensor_utils/utils.lua | 13 ++++ .../test/test_unsupported_unit_conversion.lua | 63 +++++++++++++++++++ 4 files changed, 132 insertions(+), 23 deletions(-) create mode 100644 drivers/SmartThings/matter-sensor/src/test/test_unsupported_unit_conversion.lua diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua index 345f7c5782..79315fc999 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_handlers/attribute_handlers.lua @@ -4,6 +4,7 @@ local st_utils = require "st.utils" local capabilities = require "st.capabilities" local aqs_fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" +local aqs_utils = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.utils" local AirQualitySensorAttributeHandlers = {} @@ -24,19 +25,16 @@ end function AirQualitySensorAttributeHandlers.measured_value_factory(capability_name, attribute, target_unit) return function(driver, device, ib, response) - if ib.data.value then - local reporting_unit = device:get_field(capability_name.."_unit") or aqs_fields.unit_default[capability_name] - local conversion_function = aqs_fields.conversion_tables[reporting_unit][target_unit] - if conversion_function then - local converted_value = conversion_function(ib.data.value) - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = converted_value, unit = aqs_fields.unit_strings[target_unit]})) - -- handle case where device profile supports both fineDustLevel and dustLevel + if not ib.data.value then return end + local reporting_unit = device:get_field(capability_name.."_unit") or aqs_fields.unit_default[capability_name] + local converted_value = aqs_utils.convert_value_to_unit(ib.data.value, reporting_unit, target_unit, capability_name) + + if converted_value then + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = converted_value, unit = aqs_fields.unit_strings[target_unit]})) + -- handle case where device profile supports both fineDustLevel and dustLevel if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = converted_value, unit = aqs_fields.unit_strings[target_unit]})) end - else - device.log.info_with({hub_logs=true}, string.format("Unsupported unit conversion from %s to %s", aqs_fields.unit_strings[reporting_unit], aqs_fields.unit_strings[target_unit])) - end end end end diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua index 1e4658c99e..538af70968 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/fields.lua @@ -1,8 +1,9 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local log = require "log" local version = require "version" -local utils = require "st.utils" +local st_utils = require "st.utils" local clusters = require "st.matter.clusters" local capabilities = require "st.capabilities" @@ -149,30 +150,64 @@ AirQualitySensorFields.level_strings = { [clusters.CarbonMonoxideConcentrationMeasurement.types.LevelValueEnum.CRITICAL] = "hazardous", } -AirQualitySensorFields.conversion_tables = { +AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR = 24.45 + +-- measured in g/mol +AirQualitySensorFields.molecular_weights = { + [capabilities.carbonDioxideMeasurement.NAME] = 44.010, + [capabilities.nitrogenDioxideMeasurement.NAME] = 28.014, + [capabilities.ozoneMeasurement.NAME] = 48.0, + [capabilities.formaldehydeMeasurement.NAME] = 30.031, + [capabilities.veryFineDustSensor.NAME] = "N/A", + [capabilities.fineDustSensor.NAME] = "N/A", + [capabilities.dustSensor.NAME] = "N/A", + [capabilities.radonMeasurement.NAME] = 222.018, + [capabilities.tvocMeasurement.NAME] = "N/A", +} + +local function is_valid_molecular_weight(molecular_weight) + if type(molecular_weight) ~= "number" or molecular_weight <= 0 then + log.warn_with({hub_logs = true}, string.format("unit conversion molecular weight (%s) is not a valid number", molecular_weight)) + return false + end + return true +end + +AirQualitySensorFields.unit_conversion = { [units.PPM] = { - [units.PPM] = function(value) return utils.round(value) end, - [units.PPB] = function(value) return utils.round(value * (10^3)) end - }, + [units.PPM] = function(value) return st_utils.round(value) end, + [units.PPB] = function(value) return st_utils.round(value * (10^3)) end, + [units.UGM3] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then + return st_utils.round((value * molecular_weight * 10^3) / AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR) end + end, + [units.MGM3] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then + return st_utils.round((value * molecular_weight) / AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR) end + end,}, [units.PPB] = { - [units.PPM] = function(value) return utils.round(value/(10^3)) end, - [units.PPB] = function(value) return utils.round(value) end + [units.PPM] = function(value) return st_utils.round(value/(10^3)) end, + [units.PPB] = function(value) return st_utils.round(value) end, }, [units.PPT] = { - [units.PPM] = function(value) return utils.round(value/(10^6)) end + [units.PPM] = function(value) return st_utils.round(value/(10^6)) end }, [units.MGM3] = { - [units.UGM3] = function(value) return utils.round(value * (10^3)) end + [units.UGM3] = function(value) return st_utils.round(value * (10^3)) end, + [units.PPM] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then + return st_utils.round((value * AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end + end, }, [units.UGM3] = { - [units.UGM3] = function(value) return utils.round(value) end + [units.UGM3] = function(value) return st_utils.round(value) end, + [units.PPM] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then + return st_utils.round((value * AirQualitySensorFields.MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end + end, }, [units.NGM3] = { - [units.UGM3] = function(value) return utils.round(value/(10^3)) end + [units.UGM3] = function(value) return st_utils.round(value/(10^3)) end }, [units.BQM3] = { - [units.PCIL] = function(value) return utils.round(value/37) end - } + [units.PCIL] = function(value) return st_utils.round(value/37) end + }, } return AirQualitySensorFields diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua index c11ac46605..2e8d9e487b 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua @@ -1,6 +1,7 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local log = require "log" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local embedded_cluster_utils = require "sensor_utils.embedded_cluster_utils" @@ -77,6 +78,18 @@ function AirQualitySensorUtils.set_supported_health_concern_values(device) end end +function AirQualitySensorUtils.convert_value_to_unit(value, from_unit, to_unit, capability_name) + local conversion_function = fields.unit_conversion[from_unit] and fields.unit_conversion[from_unit][to_unit] + if not conversion_function then + log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", fields.unit_strings[from_unit], fields.unit_strings[to_unit])) + return + elseif type(value) ~= "number" then + log.info_with( {hub_logs = true} , string.format("unit conversion value (%s) is not a number", value)) + return + end + return conversion_function(value, fields.molecular_weights[capability_name]) +end + --- Deeply compare two values. --- Handles metatables. Can optionally ignore cycle checking and/or function differences. --- diff --git a/drivers/SmartThings/matter-sensor/src/test/test_unsupported_unit_conversion.lua b/drivers/SmartThings/matter-sensor/src/test/test_unsupported_unit_conversion.lua new file mode 100644 index 0000000000..2e79f88dd7 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/test/test_unsupported_unit_conversion.lua @@ -0,0 +1,63 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" + +-- Unit conversion utilities for sensor +local air_quality_sensor_utils = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.utils" +local air_quality_sensor_fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" + +local units = air_quality_sensor_fields.units + +test.register_coroutine_test( + "Unsupported unit conversion from PPT to MGM3 should return nil", + function() + local result = air_quality_sensor_utils.convert_value_to_unit(100, units.PPT, units.MGM3, capabilities.ozoneMeasurement.NAME) + assert(result == nil, "Expected nil for unsupported PPT to MGM3 conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Unsupported unit conversion from PPB to UGM3 should return nil", + function() + local result = air_quality_sensor_utils.convert_value_to_unit(100, units.PPB, units.UGM3, capabilities.carbonMonoxideMeasurement.NAME) + assert(result == nil, "Expected nil for unsupported PPB to UGM3 conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Unsupported unit conversion from NGM3 to PPM should return nil", + function() + local result = air_quality_sensor_utils.convert_value_to_unit(100, units.NGM3, units.PPM, capabilities.carbonMonoxideMeasurement.NAME) + assert(result == nil, "Expected nil for unsupported NGM3 to PPM conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Supported unit conversion from PPM to PPB should return correct value", + function() + local result = air_quality_sensor_utils.convert_value_to_unit(100, units.PPM, units.PPB, capabilities.carbonMonoxideMeasurement.NAME) + assert(result ~= nil, "Expected a value for supported PPM to PPB conversion") + assert(result == 100000, "Expected 100000 for PPM to PPB conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Supported unit conversion from PPB to PPM should return correct value", + function() + local result = air_quality_sensor_utils.convert_value_to_unit(1000, units.PPB, units.PPM, capabilities.carbonMonoxideMeasurement.NAME) + assert(result ~= nil, "Expected a value for supported PPB to PPM conversion") + assert(result == 1, "Expected 1 for PPB to PPM conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Unit conversion with non-numeric value should return nil", + function() + local result = air_quality_sensor_utils.convert_value_to_unit("not a number", units.PPM, units.PPB, capabilities.carbonMonoxideMeasurement.NAME) + assert(result == nil, "Expected nil when value is not a number, got: " .. tostring(result)) + end +) + +test.run_registered_tests() From 296226b93577eb424fcfd4639ce703846d8650db Mon Sep 17 00:00:00 2001 From: Harrison Carter Date: Mon, 13 Jul 2026 12:59:45 -0500 Subject: [PATCH 072/102] Matter Thermostat: Handle invalid sensor unit conversions --- .../test/test_unsupported_unit_conversion.lua | 63 +++++++++++++++++++ .../attribute_handlers.lua | 20 ++---- .../src/thermostat_utils/fields.lua | 28 +++++++-- .../src/thermostat_utils/utils.lua | 11 ++-- 4 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 drivers/SmartThings/matter-thermostat/src/test/test_unsupported_unit_conversion.lua diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_unsupported_unit_conversion.lua b/drivers/SmartThings/matter-thermostat/src/test/test_unsupported_unit_conversion.lua new file mode 100644 index 0000000000..6c29de18a9 --- /dev/null +++ b/drivers/SmartThings/matter-thermostat/src/test/test_unsupported_unit_conversion.lua @@ -0,0 +1,63 @@ +-- Copyright © 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local capabilities = require "st.capabilities" + +-- Unit conversion utilities for thermostat +local thermostat_utils = require "thermostat_utils.utils" +local thermostat_fields = require "thermostat_utils.fields" + +local units = thermostat_fields.units + +test.register_coroutine_test( + "Unsupported unit conversion from PPT to MGM3 should return nil", + function() + local result = thermostat_utils.convert_value_to_unit(100, units.PPT, units.MGM3, capabilities.ozoneMeasurement.NAME) + assert(result == nil, "Expected nil for unsupported PPT to MGM3 conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Unsupported unit conversion from PPB to UGM3 should return nil", + function() + local result = thermostat_utils.convert_value_to_unit(100, units.PPB, units.UGM3, capabilities.carbonMonoxideMeasurement.NAME) + assert(result == nil, "Expected nil for unsupported PPB to UGM3 conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Unsupported unit conversion from NGM3 to PPM should return nil", + function() + local result = thermostat_utils.convert_value_to_unit(100, units.NGM3, units.PPM, capabilities.carbonMonoxideMeasurement.NAME) + assert(result == nil, "Expected nil for unsupported NGM3 to PPM conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Supported unit conversion from PPM to PPB should return correct value", + function() + local result = thermostat_utils.convert_value_to_unit(100, units.PPM, units.PPB, capabilities.carbonMonoxideMeasurement.NAME) + assert(result ~= nil, "Expected a value for supported PPM to PPB conversion") + assert(result == 100000, "Expected 100000 for PPM to PPB conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Supported unit conversion from PPB to PPM should return correct value", + function() + local result = thermostat_utils.convert_value_to_unit(1000, units.PPB, units.PPM, capabilities.carbonMonoxideMeasurement.NAME) + assert(result ~= nil, "Expected a value for supported PPB to PPM conversion") + assert(result == 1, "Expected 1 for PPB to PPM conversion, got: " .. tostring(result)) + end +) + +test.register_coroutine_test( + "Unit conversion with non-numeric value should return nil", + function() + local result = thermostat_utils.convert_value_to_unit("not a number", units.PPM, units.PPB, capabilities.carbonMonoxideMeasurement.NAME) + assert(result == nil, "Expected nil when value is not a number, got: " .. tostring(result)) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua index f0a210bc16..e5b3728b61 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_handlers/attribute_handlers.lua @@ -494,23 +494,15 @@ end function AttributeHandlers.concentration_measured_value_factory(capability_name, attribute, target_unit) return function(driver, device, ib, response) - local reporting_unit = device:get_field(capability_name.."_unit") + if not ib.data.value then return end + local reporting_unit = device:get_field(capability_name.."_unit") or fields.unit_default[capability_name] + local converted_value = thermostat_utils.convert_value_to_unit(ib.data.value, reporting_unit, target_unit, capability_name) - if not reporting_unit then - reporting_unit = fields.unit_default[capability_name] - device:set_field(capability_name.."_unit", reporting_unit, {persist = true}) - end - - local value = nil - if reporting_unit then - value = thermostat_utils.unit_conversion(ib.data.value, reporting_unit, target_unit, capability_name) - end - - if value then - device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = value, unit = fields.unit_strings[target_unit]})) + if converted_value then + device:emit_event_for_endpoint(ib.endpoint_id, attribute({value = converted_value, unit = fields.unit_strings[target_unit]})) -- handle case where device profile supports both fineDustLevel and dustLevel if capability_name == capabilities.fineDustSensor.NAME and device:supports_capability(capabilities.dustSensor) then - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = value, unit = fields.unit_strings[target_unit]})) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.dustSensor.fineDustLevel({value = converted_value, unit = fields.unit_strings[target_unit]})) end end end diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua index 770fd7d65e..ddedab385d 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/fields.lua @@ -1,6 +1,7 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local log = require "log" local version = require "version" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" @@ -205,13 +206,24 @@ ThermostatFields.molecular_weights = { [capabilities.tvocMeasurement.NAME] = "N/A", } -ThermostatFields.conversion_tables = { +local function is_valid_molecular_weight(molecular_weight) + if type(molecular_weight) ~= "number" or molecular_weight <= 0 then + log.warn_with({hub_logs = true}, string.format("unit conversion molecular weight (%s) is not a valid number", molecular_weight)) + return false + end + return true +end + +ThermostatFields.unit_conversion = { [units.PPM] = { [units.PPM] = function(value) return st_utils.round(value) end, [units.PPB] = function(value) return st_utils.round(value * (10^3)) end, - [units.UGM3] = function(value, molecular_weight) return st_utils.round((value * molecular_weight * 10^3) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end, - [units.MGM3] = function(value, molecular_weight) return st_utils.round((value * molecular_weight) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end, - }, + [units.UGM3] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then + return st_utils.round((value * molecular_weight * 10^3) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end + end, + [units.MGM3] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then + return st_utils.round((value * molecular_weight) / ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) end + end,}, [units.PPB] = { [units.PPM] = function(value) return st_utils.round(value/(10^3)) end, [units.PPB] = function(value) return st_utils.round(value) end, @@ -221,11 +233,15 @@ ThermostatFields.conversion_tables = { }, [units.MGM3] = { [units.UGM3] = function(value) return st_utils.round(value * (10^3)) end, - [units.PPM] = function(value, molecular_weight) return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end, + [units.PPM] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then + return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / molecular_weight) end + end, }, [units.UGM3] = { [units.UGM3] = function(value) return st_utils.round(value) end, - [units.PPM] = function(value, molecular_weight) return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end, + [units.PPM] = function(value, molecular_weight) if is_valid_molecular_weight(molecular_weight) then + return st_utils.round((value * ThermostatFields.MGM3_PPM_CONVERSION_FACTOR) / (molecular_weight * 10^3)) end + end, }, [units.NGM3] = { [units.UGM3] = function(value) return st_utils.round(value/(10^3)) end diff --git a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua index a233a0c0e7..940fe9835e 100644 --- a/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua +++ b/drivers/SmartThings/matter-thermostat/src/thermostat_utils/utils.lua @@ -180,18 +180,15 @@ function ThermostatUtils.get_device_type(device) return main_device_type end -function ThermostatUtils.unit_conversion(value, from_unit, to_unit, capability_name) - local conversion_function = fields.conversion_tables[from_unit] and fields.conversion_tables[from_unit][to_unit] or nil +function ThermostatUtils.convert_value_to_unit(value, from_unit, to_unit, capability_name) + local conversion_function = fields.unit_conversion[from_unit] and fields.unit_conversion[from_unit][to_unit] if not conversion_function then log.info_with( {hub_logs = true} , string.format("Unsupported unit conversion from %s to %s", fields.unit_strings[from_unit], fields.unit_strings[to_unit])) return - end - - if not value then - log.info_with( {hub_logs = true} , "unit conversion value is nil") + elseif type(value) ~= "number" then + log.info_with( {hub_logs = true} , string.format("unit conversion value (%s) is not a number", value)) return end - return conversion_function(value, fields.molecular_weights[capability_name]) end From 311711d614c67df789b4307a22b592e5988cc68a Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 27 Jul 2026 10:21:47 -0500 Subject: [PATCH 073/102] Matter Window Covering: Always report accurate capability operational state (#3133) --- .../matter-window-covering/src/init.lua | 414 +++++---- .../src/sub_drivers.lua | 1 - .../can_handle.lua | 19 - .../fingerprints.lua | 8 - .../init.lua | 126 --- .../src/test/test_matter_window_covering.lua | 822 +++++++----------- 6 files changed, 550 insertions(+), 840 deletions(-) delete mode 100644 drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua delete mode 100644 drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua delete mode 100644 drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua diff --git a/drivers/SmartThings/matter-window-covering/src/init.lua b/drivers/SmartThings/matter-window-covering/src/init.lua index bfde1e3fc8..ba03737a73 100644 --- a/drivers/SmartThings/matter-window-covering/src/init.lua +++ b/drivers/SmartThings/matter-window-covering/src/init.lua @@ -6,26 +6,134 @@ --Note: No support for setting device into calibration mode, it must be done manually local capabilities = require "st.capabilities" local im = require "st.matter.interaction_model" -local log = require "log" local clusters = require "st.matter.clusters" local MatterDriver = require "st.matter.driver" local utils = require "st.utils" -local CURRENT_LIFT = "__current_lift" -local CURRENT_TILT = "__current_tilt" local battery_support = { NO_BATTERY = "NO_BATTERY", BATTERY_LEVEL = "BATTERY_LEVEL", BATTERY_PERCENTAGE = "BATTERY_PERCENTAGE" } -local REVERSE_POLARITY = "__reverse_polarity" -local PRESET_LEVEL_KEY = "__preset_level_key" +local OP_STATUS_BITMAP = { + idle = 0x00, + opening = 0x01, + closing = 0x02 +} local DEFAULT_PRESET_LEVEL = 50 -local LATEST_TARGET_LEVEL = "_latest_target_level" -local TARGET_LEVEL_TIME_OUT = "_target_level_timeout" -local TARGET_LEVEL_TIME_OUT_SECONDS = 15 -local TARGET_REACH_TOLERANCE = 1 +local TARGET_LIFT_PERCENT = { + TIMEOUT_DELAY_S = 120, + TIMEOUT = "__target_lift_percent_timeout", + STATE = "__target_lift_percent" +} + +local TARGET_TILT_PERCENT = { + TIMEOUT_DELAY_S = 120, + TIMEOUT = "__target_tilt_percent_timeout", + STATE = "__target_tilt_percent" +} + +local TARGET_REACH_TOLERANCE = 0.5 + +--- Gets the current window shade status based on the lift and tilt positions. +---@param device any a Matter device object +---@return any status the current window shade status +local function get_idle_window_shade_status(device) + -- Update the window shade status according to the lift and tilt positions. + -- LIFT TILT Window Shade + -- 100 any Open + -- 1-99 any Partially Open + -- 0 1-100 Partially Open + -- 0 0 Closed + -- 0 nil Closed + -- nil 100 Open + -- nil 1-99 Partially Open + -- nil 0 Closed + -- Note that lift or tilt may be nil if either the window shade does not + -- support them or if they haven't been received from a device report yet. + + local lift_position = device:get_latest_state( + "main", + capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME + ) + local tilt_position = device:get_latest_state( + "main", + capabilities.windowShadeTiltLevel.ID, + capabilities.windowShadeTiltLevel.shadeTiltLevel.NAME + ) + local reverse = device.preferences.reverse + local windowShade = capabilities.windowShade.windowShade + local status_event = windowShade.unknown() + + if lift_position == nil then + if tilt_position == 0 then + status_event = reverse and windowShade.open() or windowShade.closed() + elseif tilt_position == 100 then + status_event = reverse and windowShade.closed() or windowShade.open() + else + status_event = windowShade.partially_open() + end + elseif lift_position == 100 then + status_event = reverse and windowShade.closed() or windowShade.open() + elseif lift_position > 0 then + status_event = windowShade.partially_open() + elseif lift_position == 0 then + if tilt_position == nil or tilt_position == 0 then + status_event = reverse and windowShade.open() or windowShade.closed() + elseif tilt_position > 0 then + status_event = windowShade.partially_open() + end + end + return status_event +end + +--- Clears the cached target percentage and cancels any associated timeout. +--- @param device any a Matter device object +--- @param op_fields table a table containing operational field constants used for caching target percentages and timeouts. +local function clear_cached_data(device, op_fields) + local current_timer = device:get_field(op_fields.TIMEOUT) + if current_timer then + device.thread:cancel_timer(current_timer) + end + device:set_field(op_fields.STATE, nil) + device:set_field(op_fields.TIMEOUT, nil) +end + +--- Tries to emit the idle window shade status if no target percentages are set. +--- @param device any a Matter device object +local function try_emit_idle_window_shade_status(device) + if not device:get_field(TARGET_LIFT_PERCENT.STATE) and + not device:get_field(TARGET_TILT_PERCENT.STATE) then + device:emit_event(get_idle_window_shade_status(device)) + end +end + +--- Caches the target percentage for a device and sets a timeout to clear it. +--- @param device any a Matter device object +--- @param data number the target percentage to cache +--- @param op_fields table a table containing operational field constants used for caching target percentages and timeouts. +local function cache_data_with_timeout(device, data, op_fields) + device:set_field(op_fields.STATE, data) + local previous_timer = device:get_field(op_fields.TIMEOUT) + if previous_timer then + device.thread:cancel_timer(previous_timer) + end + local new_timer = device.thread:call_with_delay(op_fields.TIMEOUT_DELAY_S, function() + device:set_field(op_fields.STATE, nil) + device:set_field(op_fields.TIMEOUT, nil) + try_emit_idle_window_shade_status(device) + end) + device:set_field(op_fields.TIMEOUT, new_timer) +end + +local function is_target_value_reached(current_value, target_value) + if (target_value and math.abs(current_value - target_value) <= TARGET_REACH_TOLERANCE) then + return true + end + return false +end local function find_default_endpoint(device, cluster) local res = device.MATTER_DEFAULT_ENDPOINT @@ -66,17 +174,18 @@ local function match_profile(device, battery_supported) end local function device_init(driver, device) + -- set unused fields to nil. These field settings can be removed after a single driver update. + device:set_field("__preset_level_key", nil) + device:set_field("__reverse_polarity", nil) + device:set_component_to_endpoint_fn(component_to_endpoint) if device:supports_capability_by_id(capabilities.windowShadePreset.ID) and device:get_latest_state("main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME) == nil then -- These should only ever be nil once (and at the same time) for already-installed devices -- It can be removed after migration is complete device:emit_event(capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed = false}})) - local preset_position = device:get_field(PRESET_LEVEL_KEY) or - (device.preferences ~= nil and device.preferences.presetPosition) or - DEFAULT_PRESET_LEVEL + local preset_position = device.preferences.presetPosition or DEFAULT_PRESET_LEVEL device:emit_event(capabilities.windowShadePreset.position(preset_position, {visibility = {displayed = false}})) - device:set_field(PRESET_LEVEL_KEY, preset_position, {persist = true}) end device:subscribe() end @@ -94,17 +203,8 @@ end local function info_changed(driver, device, event, args) if device.profile.id ~= args.old_st_store.profile.id then - -- Profile has changed, resubscribe device:subscribe() - elseif args.old_st_store.preferences.reverse ~= device.preferences.reverse then - if device.preferences.reverse then - device:set_field(REVERSE_POLARITY, true, { persist = true }) - else - device:set_field(REVERSE_POLARITY, false, { persist = true }) - end - else - -- Something else has changed info (SW update, reinterview, etc.), so - -- try updating profile as needed + elseif device.matter_version.software ~= args.old_st_store.matter_version.software then local battery_feature_eps = device:get_endpoints(clusters.PowerSource.ID, {feature_bitmap = clusters.PowerSource.types.PowerSourceFeature.BATTERY}) if #battery_feature_eps > 0 then local attribute_list_read = im.InteractionRequest(im.InteractionRequest.RequestType.READ, {}) @@ -120,17 +220,18 @@ local function device_added(driver, device) device:emit_event( capabilities.windowShade.supportedWindowShadeCommands({"open", "close", "pause"}, {visibility = {displayed = false}}) ) - device:set_field(REVERSE_POLARITY, false, { persist = true }) end -local function device_removed(driver, device) log.info("device removed") end - -- capability handlers local function handle_preset(driver, device, cmd) - local lift_value = device:get_latest_state( - "main", capabilities.windowShadePreset.ID, capabilities.windowShadePreset.position.NAME - ) or DEFAULT_PRESET_LEVEL - local hundredths_lift_percent = (100 - lift_value) * 100 + local preset_target_percent = device:get_latest_state( + "main", + capabilities.windowShadePreset.ID, + capabilities.windowShadePreset.position.NAME, + DEFAULT_PRESET_LEVEL + ) + cache_data_with_timeout(device, preset_target_percent, TARGET_LIFT_PERCENT) + local hundredths_lift_percent = (100 - preset_target_percent) * 100 local endpoint_id = device:component_to_endpoint(cmd.component) device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage( device, endpoint_id, hundredths_lift_percent @@ -139,191 +240,157 @@ end local function handle_set_preset(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) - device:set_field(PRESET_LEVEL_KEY, cmd.args.position) device:emit_event_for_endpoint(endpoint_id, capabilities.windowShadePreset.position(cmd.args.position)) end --- close covering 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:get_field(REVERSE_POLARITY) then - req = clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) + if #device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) > 0 then + cache_data_with_timeout(device, device.preferences.reverse and 100 or 0, TARGET_LIFT_PERCENT) end - device:send(req) + if #device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) > 0 then + cache_data_with_timeout(device, device.preferences.reverse and 100 or 0, TARGET_TILT_PERCENT) + end + device:send(device.preferences.reverse and + clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) or + clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) + ) end --- open covering 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:get_field(REVERSE_POLARITY) then - req = clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) + if #device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.LIFT}) > 0 then + cache_data_with_timeout(device, device.preferences.reverse and 0 or 100, TARGET_LIFT_PERCENT) + end + if #device:get_endpoints(clusters.WindowCovering.ID, {feature_bitmap = clusters.WindowCovering.types.Feature.TILT}) > 0 then + cache_data_with_timeout(device, device.preferences.reverse and 0 or 100, TARGET_TILT_PERCENT) end - device:send(req) + device:send(device.preferences.reverse and + clusters.WindowCovering.server.commands.DownOrClose(device, endpoint_id) or + clusters.WindowCovering.server.commands.UpOrOpen(device, endpoint_id) + ) end --- pause covering 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) + device:send(clusters.WindowCovering.server.commands.StopMotion(device, endpoint_id)) end -- move to shade level between 0-100 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) + local hundredths_lift_percentage = (100 - cmd.args.shadeLevel) * 100 + cache_data_with_timeout(device, cmd.args.shadeLevel, TARGET_LIFT_PERCENT) + device:send(clusters.WindowCovering.server.commands.GoToLiftPercentage( + device, endpoint_id, hundredths_lift_percentage + )) end local function handle_step_shade_level(driver, device, cmd) local step = cmd.args.stepSize - - local current_level = device:get_field(LATEST_TARGET_LEVEL) or - device:get_latest_state("main", capabilities.windowShadeLevel.ID, - capabilities.windowShadeLevel.shadeLevel.NAME) or 0 - - local target_level = utils.clamp_value(current_level + step, 0, 100) - - device:set_field(LATEST_TARGET_LEVEL, target_level) - - local old_timer = device:get_field(TARGET_LEVEL_TIME_OUT) - if old_timer ~= nil then - device.thread:cancel_timer(old_timer) - end - - -- Set 15 second timeout timer to ensure target_level is cleared - local timer = device.thread:call_with_delay(TARGET_LEVEL_TIME_OUT_SECONDS, function(d) - device:set_field(LATEST_TARGET_LEVEL, nil) - end) - device:set_field(TARGET_LEVEL_TIME_OUT, timer) - + local latest_lift_percentage = device:get_latest_state( + "main", + capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME, + 0 + ) + local target_lift_percent = device:get_field(TARGET_LIFT_PERCENT.STATE) or latest_lift_percentage + local updated_target_lift = utils.clamp_value(target_lift_percent + step, 0, 100) + cache_data_with_timeout(device, updated_target_lift, TARGET_LIFT_PERCENT) driver:inject_capability_command(device, { capability = capabilities.windowShadeLevel.ID, component = cmd.component, command = capabilities.windowShadeLevel.commands.setShadeLevel.NAME, - named_args = { shadeLevel = target_level } + named_args = { shadeLevel = updated_target_lift } }) end -- move to shade tilt level between 0-100 local function handle_shade_tilt_level(driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) - local tilt_percentage_value = 100 - cmd.args.level - local hundredths_tilt_percentage = tilt_percentage_value * 100 + local hundredths_tilt_percentage = (100 - cmd.args.level) * 100 + cache_data_with_timeout(device, cmd.args.level, TARGET_TILT_PERCENT) device:send(clusters.WindowCovering.server.commands.GoToTiltPercentage( device, endpoint_id, hundredths_tilt_percentage )) end --- current lift/tilt percentage, changed to 100ths percent -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) - - -- Step control logic - local target_level_field = device:get_field(LATEST_TARGET_LEVEL) - if target_level_field and attribute == capabilities.windowShadeLevel.shadeLevel then - -- Allow ±1 degree tolerance for reaching target - if math.abs(position - target_level_field) <= TARGET_REACH_TOLERANCE then - -- Device reached target position, clear target marker and timeout timer - device:set_field(LATEST_TARGET_LEVEL, nil) - local timer = device:get_field(TARGET_LEVEL_TIME_OUT) - if timer ~= nil then - device.thread:cancel_timer(timer) - device:set_field(TARGET_LEVEL_TIME_OUT, nil) - end - end - end +-- attribute handlers +local function current_position_lift_percent_100ths_handler(driver, device, ib, response) + if not ib.data.value then return end + local lift_percent = 100 - math.floor(ib.data.value / 100) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(lift_percent)) + + if is_target_value_reached(lift_percent, device:get_field(TARGET_LIFT_PERCENT.STATE)) then + clear_cached_data(device, TARGET_LIFT_PERCENT) + try_emit_idle_window_shade_status(device) + end +end - local reverse = device:get_field(REVERSE_POLARITY) - device:emit_event_for_endpoint(ib.endpoint_id, attribute(position)) +local function current_position_tilt_percent_100ths_handler(driver, device, ib, response) + if not ib.data.value then return end + local tilt_percent = 100 - math.floor(ib.data.value / 100) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeTiltLevel.shadeTiltLevel(tilt_percent)) - if attribute == capabilities.windowShadeLevel.shadeLevel then - device:set_field(CURRENT_LIFT, position) + if is_target_value_reached(tilt_percent, device:get_field(TARGET_TILT_PERCENT.STATE)) then + clear_cached_data(device, TARGET_TILT_PERCENT) + try_emit_idle_window_shade_status(device) + end +end + +local function target_position_lift_percent_100ths_handler(driver, device, ib, response) + if type(ib.data.value) == "number" then + local target_lift_percent = 100 - math.floor(ib.data.value / 100) + local latest_lift_percentage = device:get_latest_state( + "main", + capabilities.windowShadeLevel.ID, + capabilities.windowShadeLevel.shadeLevel.NAME + ) + if latest_lift_percentage and is_target_value_reached(latest_lift_percentage, target_lift_percent) then + clear_cached_data(device, TARGET_LIFT_PERCENT) + try_emit_idle_window_shade_status(device) else - device:set_field(CURRENT_TILT, position) + cache_data_with_timeout(device, target_lift_percent, TARGET_LIFT_PERCENT) end + end +end - local lift_position = device:get_field(CURRENT_LIFT) - local tilt_position = device:get_field(CURRENT_TILT) - - -- Update the window shade status according to the lift and tilt positions. - -- LIFT TILT Window Shade - -- 100 any Open - -- 1-99 any Partially Open - -- 0 1-100 Partially Open - -- 0 0 Closed - -- 0 nil Closed - -- nil 100 Open - -- nil 1-99 Partially Open - -- nil 0 Closed - -- Note that lift or tilt may be nil if either the window shade does not - -- support them or if they haven't been received from a device report yet. - - if lift_position == nil then - if tilt_position == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) - elseif tilt_position == 100 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closed() or windowShade.open()) - else - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) - end - - elseif 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 - if tilt_position == nil or tilt_position == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) - elseif tilt_position > 0 then - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) - end +local function target_position_tilt_percent_100ths_handler(driver, device, ib, response) + if type(ib.data.value) == "number" then + local target_tilt_percent = 100 - math.floor(ib.data.value / 100) + local latest_tilt_percent = device:get_latest_state( + "main", + capabilities.windowShadeTiltLevel.ID, + capabilities.windowShadeTiltLevel.shadeTiltLevel.NAME + ) + if latest_tilt_percent and is_target_value_reached(latest_tilt_percent, target_tilt_percent) then + clear_cached_data(device, TARGET_TILT_PERCENT) + try_emit_idle_window_shade_status(device) + else + cache_data_with_timeout(device, target_tilt_percent, TARGET_TILT_PERCENT) end end end -- checks the current position of the shade -local function current_status_handler(driver, device, ib, response) +local function operational_status_handler(driver, device, ib, response) + if not ib.data.value then return end + local global_op_status = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL + + local reverse = device.preferences.reverse local windowShade = capabilities.windowShade.windowShade - local reverse = device:get_field(REVERSE_POLARITY) - local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL - if state == 1 then -- opening - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closing() or windowShade.opening()) - elseif state == 2 then -- closing - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.opening() or windowShade.closing()) - elseif state == 0 then -- stopped (idle) - -- Clear target_level when device stops moving - local target_level_field = device:get_field(LATEST_TARGET_LEVEL) - if target_level_field then - device:set_field(LATEST_TARGET_LEVEL, nil) - local timer = device:get_field(TARGET_LEVEL_TIME_OUT) - if timer ~= nil then - device.thread:cancel_timer(timer) - end - end - elseif state ~= 0 then -- unknown - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) + local status_event + if global_op_status == OP_STATUS_BITMAP.idle then + try_emit_idle_window_shade_status(device) + elseif global_op_status == OP_STATUS_BITMAP.opening then + status_event = reverse and windowShade.closing() or windowShade.opening() + elseif global_op_status == OP_STATUS_BITMAP.closing then + status_event = reverse and windowShade.opening() or windowShade.closing() + else + status_event = windowShade.unknown() end -end - -local function level_attr_handler(driver, device, ib, response) - if ib.data.value ~= nil then - --TODO should we invert this like we do for CurrentLiftPercentage100ths? - local level = math.floor((ib.data.value / 254.0 * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(level)) + if status_event then + device:emit_event_for_endpoint(ib.endpoint_id, status_event) end end @@ -344,7 +411,7 @@ local function battery_charge_level_attr_handler(driver, device, ib, response) end local function power_source_attribute_list_handler(driver, device, ib, response) - for _, attr in ipairs(ib.data.elements) do + for _, attr in ipairs(ib.data.elements or {}) do -- Re-profile the device if BatPercentRemaining (Attribute ID 0x0C) is present. if attr.value == 0x0C then match_profile(device, battery_support.BATTERY_PERCENTAGE) @@ -359,23 +426,18 @@ end local matter_driver_template = { lifecycle_handlers = { init = device_init, - removed = device_removed, added = device_added, infoChanged = info_changed, doConfigure = do_configure }, matter_handlers = { attr = { - --TODO LevelControl may not be needed for certified devices since - -- certified should use CurrentPositionLiftPercent100ths attr - [clusters.LevelControl.ID] = { - [clusters.LevelControl.attributes.CurrentLevel.ID] = level_attr_handler, - }, [clusters.WindowCovering.ID] = { - --uses percent100ths more often - [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_pos_handler(capabilities.windowShadeLevel.shadeLevel), - [clusters.WindowCovering.attributes.CurrentPositionTiltPercent100ths.ID] = current_pos_handler(capabilities.windowShadeTiltLevel.shadeTiltLevel), - [clusters.WindowCovering.attributes.OperationalStatus.ID] = current_status_handler, + [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_position_lift_percent_100ths_handler, + [clusters.WindowCovering.attributes.CurrentPositionTiltPercent100ths.ID] = current_position_tilt_percent_100ths_handler, + [clusters.WindowCovering.attributes.TargetPositionLiftPercent100ths.ID] = target_position_lift_percent_100ths_handler, + [clusters.WindowCovering.attributes.TargetPositionTiltPercent100ths.ID] = target_position_tilt_percent_100ths_handler, + [clusters.WindowCovering.attributes.OperationalStatus.ID] = operational_status_handler, }, [clusters.PowerSource.ID] = { [clusters.PowerSource.attributes.AttributeList.ID] = power_source_attribute_list_handler, @@ -391,9 +453,11 @@ local matter_driver_template = { [capabilities.windowShadeLevel.ID] = { clusters.LevelControl.attributes.CurrentLevel, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths, + clusters.WindowCovering.attributes.TargetPositionLiftPercent100ths, }, [capabilities.windowShadeTiltLevel.ID] = { clusters.WindowCovering.attributes.CurrentPositionTiltPercent100ths, + clusters.WindowCovering.attributes.TargetPositionTiltPercent100ths, }, [capabilities.battery.ID] = { clusters.PowerSource.attributes.BatPercentRemaining diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua index ad3e53e4b0..3a833f39bc 100644 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua +++ b/drivers/SmartThings/matter-window-covering/src/sub_drivers.lua @@ -4,6 +4,5 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { lazy_load_if_possible("sub_drivers.closure"), - lazy_load_if_possible("sub_drivers.matter-window-covering-position-updates-while-moving"), } return sub_drivers diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua deleted file mode 100644 index e3e12116e1..0000000000 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/can_handle.lua +++ /dev/null @@ -1,19 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local function is_matter_window_covering_position_updates_while_moving(opts, driver, device) - local device_lib = require "st.device" - if device.network_type ~= device_lib.NETWORK_TYPE_MATTER then - return false - end - local FINGERPRINTS = require("sub_drivers.matter-window-covering-position-updates-while-moving.fingerprints") - for i, v in ipairs(FINGERPRINTS) do - if device.manufacturer_info.vendor_id == v[1] and - device.manufacturer_info.product_id == v[2] then - return true, require("sub_drivers.matter-window-covering-position-updates-while-moving") - end - end - return false -end - -return is_matter_window_covering_position_updates_while_moving diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua deleted file mode 100644 index 37800fc680..0000000000 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/fingerprints.lua +++ /dev/null @@ -1,8 +0,0 @@ --- Copyright 2025 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - -local SUB_WINDOW_COVERING_VID_PID = { - {0x10e1, 0x1005} -- VDA -} - -return SUB_WINDOW_COVERING_VID_PID diff --git a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua b/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua deleted file mode 100644 index 5db95ba28c..0000000000 --- a/drivers/SmartThings/matter-window-covering/src/sub_drivers/matter-window-covering-position-updates-while-moving/init.lua +++ /dev/null @@ -1,126 +0,0 @@ --- Copyright 2023 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - - -local capabilities = require "st.capabilities" -local clusters = require "st.matter.clusters" - -local DEFAULT_LEVEL = 0 -local STATE_MACHINE = "__state_machine" -local REVERSE_POLARITY = "__reverse_polarity" - -local StateMachineEnum = { - STATE_IDLE = 0x00, - STATE_MOVING = 0x01, - STATE_OPERATIONAL_STATE_FIRED = 0x02, - STATE_CURRENT_POSITION_FIRED = 0x03 -} - - - -local function device_init(driver, device) - device:subscribe() -end - --- current lift percentage, changed to 100ths percent -local function current_pos_handler(driver, device, ib, response) - if ib.data.value == nil then - return - end - local position = 100 - math.floor(ib.data.value / 100) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.windowShadeLevel.shadeLevel(position)) - local windowShade = capabilities.windowShade.windowShade - local reverse = device:get_field(REVERSE_POLARITY) - local state_machine = device:get_field(STATE_MACHINE) - -- When state_machine is STATE_IDLE or STATE_CURRENT_POSITION_FIRED, nothing to do - if state_machine == StateMachineEnum.STATE_MOVING then - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_CURRENT_POSITION_FIRED) - elseif state_machine == StateMachineEnum.STATE_OPERATIONAL_STATE_FIRED or state_machine == nil then - if position == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.open() or windowShade.closed()) - elseif position == 100 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and windowShade.closed() or windowShade.open()) - elseif position > 0 and position < 100 then - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.partially_open()) - else - device:emit_event_for_endpoint(ib.endpoint_id, windowShade.unknown()) - end - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) - end -end - --- checks the current position of the shade -local function current_status_handler(driver, device, ib, response) - local attr = capabilities.windowShade.windowShade - local position = device:get_latest_state( - "main", capabilities.windowShadeLevel.ID, - capabilities.windowShadeLevel.shadeLevel.NAME - ) or DEFAULT_LEVEL - for _, rb in ipairs(response.info_blocks) do - if rb.info_block.attribute_id == clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID and - rb.info_block.cluster_id == clusters.WindowCovering.ID and - rb.info_block.data ~= nil and - rb.info_block.data.value ~= nil then - position = math.floor(rb.info_block.data.value / 100) - end - end - position = 100 - position - local reverse = device:get_field(REVERSE_POLARITY) - local state = ib.data.value & clusters.WindowCovering.types.OperationalStatus.GLOBAL - local state_machine = device:get_field(STATE_MACHINE) - -- When state_machine is STATE_OPERATIONAL_STATE_FIRED, nothing to do - if state_machine == StateMachineEnum.STATE_IDLE then - if state == 1 then -- opening - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.closing() or attr.opening()) - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_MOVING) - elseif state == 2 then -- closing - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.opening() or attr.closing()) - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_MOVING) - end - elseif state_machine == StateMachineEnum.STATE_MOVING then - if state == 0 then -- not moving - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_OPERATIONAL_STATE_FIRED) - elseif state == 1 then -- opening - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.closing() or attr.opening()) - elseif state == 2 then -- closing - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.opening() or attr.closing()) - else - device:emit_event_for_endpoint(ib.endpoint_id, attr.unknown()) - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) - end - elseif state_machine == StateMachineEnum.STATE_CURRENT_POSITION_FIRED then - if state == 0 then -- not moving - if position == 100 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.closed() or attr.open()) - elseif position == 0 then - device:emit_event_for_endpoint(ib.endpoint_id, reverse and attr.open() or attr.closed()) - else - device:emit_event_for_endpoint(ib.endpoint_id, attr.partially_open()) - end - else - device:emit_event_for_endpoint(ib.endpoint_id, attr.unknown()) - end - device:set_field(STATE_MACHINE, StateMachineEnum.STATE_IDLE) - end -end - -local matter_window_covering_position_updates_while_moving_handler = { - NAME = "matter-window-covering-position-updates-while-moving", - lifecycle_handlers = { - init = device_init, - }, - matter_handlers = { - attr = { - [clusters.WindowCovering.ID] = { - [clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID] = current_pos_handler, - [clusters.WindowCovering.attributes.OperationalStatus.ID] = current_status_handler, - } - } - }, - capability_handlers = { - }, - can_handle = require("sub_drivers.matter-window-covering-position-updates-while-moving.can_handle"), - shared_device_thread_enabled = true, -} - -return matter_window_covering_position_updates_while_moving_handler diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index b65a621c83..8376e3a6d4 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -21,6 +21,7 @@ local mock_device = test.mock_device.build_test_matter_device( { profile = t_utils.get_profile_definition("window-covering-tilt-battery.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {software = 1}, endpoints = { { endpoint_id = 2, @@ -83,6 +84,8 @@ local CLUSTER_SUBSCRIBE_LIST = { clusters.LevelControl.server.attributes.CurrentLevel, WindowCovering.server.attributes.CurrentPositionLiftPercent100ths, WindowCovering.server.attributes.CurrentPositionTiltPercent100ths, + WindowCovering.server.attributes.TargetPositionLiftPercent100ths, + WindowCovering.server.attributes.TargetPositionTiltPercent100ths, WindowCovering.server.attributes.OperationalStatus, clusters.PowerSource.server.attributes.BatPercentRemaining } @@ -91,6 +94,7 @@ local CLUSTER_SUBSCRIBE_LIST_NO_BATTERY = { clusters.LevelControl.server.attributes.CurrentLevel, WindowCovering.server.attributes.CurrentPositionLiftPercent100ths, WindowCovering.server.attributes.OperationalStatus, + WindowCovering.server.attributes.TargetPositionLiftPercent100ths, } local function set_preset(device) @@ -156,24 +160,18 @@ end test.set_test_init_function(test_init) test.register_coroutine_test( - "WindowCovering OperationalStatus state closed following lift position update", function() - test.socket.capability:__set_channel_ordering("relaxed") + "Idle WindowCovering OperationalStatus state triggers 'open' capability emission if lift position is 100", function() test.socket.matter:__queue_receive( { mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, 10000 + mock_device, 10, 0 ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(0) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.closed() + "main", capabilities.windowShadeLevel.shadeLevel(100) ) ) test.socket.matter:__queue_receive( @@ -182,6 +180,11 @@ test.register_coroutine_test( WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), } ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.open() + ) + ) end, { min_api_version = 17 @@ -189,24 +192,18 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state closed following tilt position update", function() - test.socket.capability:__set_channel_ordering("relaxed") + "Idle WindowCovering OperationalStatus state triggers 'closed' capability emission if lift position is 0", function() test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( mock_device, 10, 10000 ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.closed() + "main", capabilities.windowShadeLevel.shadeLevel(0) ) ) test.socket.matter:__queue_receive( @@ -215,6 +212,11 @@ test.register_coroutine_test( WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), } ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShade.windowShade.closed() + ) + ) end, { min_api_version = 17 @@ -222,30 +224,29 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state closed before lift position 0", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), - } - ) + "Idle WindowCovering OperationalStatus state triggers 'partially open' capability emission if lift position is 50", function() test.socket.matter:__queue_receive( { mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, 10000 + mock_device, 10, 5000 ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(0) + "main", capabilities.windowShadeLevel.shadeLevel(50) ) ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.closed() + "main", capabilities.windowShade.windowShade.partially_open() ) ) end, @@ -255,30 +256,30 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state closed before tilt position 0", function() + "Idle WindowCovering OperationalStatus state triggers 'partially open' capability emission if tilt position is between 1-100", function() test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), - } - ) test.socket.matter:__queue_receive( { mock_device.id, WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( - mock_device, 10, 10000 + mock_device, 10, ((100 - 15) *100) ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(15) ) ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + } + ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.closed() + "main", capabilities.windowShade.windowShade.partially_open() ) ) end, @@ -288,32 +289,18 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state open following lift position update", function() - test.socket.capability:__set_channel_ordering("relaxed") + "Unknown WindowCovering OperationalStatus state triggers 'unknown' capability emission", function() test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, 0 - ), + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 99), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(100) + "main", capabilities.windowShade.windowShade.unknown() ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.open() - ) - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), - } - ) end, { min_api_version = 17 @@ -321,32 +308,18 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state open following tilt position update", function() - test.socket.capability:__set_channel_ordering("relaxed") + "Opening WindowCovering OperationalStatus state triggers 'closing' capability emission", function() test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( - mock_device, 10, 0 - ), + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 2), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.open() + "main", capabilities.windowShade.windowShade.closing() ) ) - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), - } - ) end, { min_api_version = 17 @@ -354,65 +327,66 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus state open before lift position event", function() - test.socket.capability:__set_channel_ordering("relaxed") + "Closing WindowCovering OperationalStatus state triggers 'opening' capability emission", function() test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 1), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(100) + "main", capabilities.windowShade.windowShade.opening() ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.open() - ) - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, 0 - ), - } - ) end, { min_api_version = 17 } ) + test.register_coroutine_test( - "WindowCovering OperationalStatus state open before tilt position event", function() - test.socket.capability:__set_channel_ordering("relaxed") + "WindowCovering CurrentPositionLiftPercent100ths triggers appropriate capability emission", function() test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 10000 + ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + "main", capabilities.windowShadeLevel.shadeLevel(0) ) ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 0 + ), + } + ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.open() + "main", capabilities.windowShadeLevel.shadeLevel(100) ) ) test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( - mock_device, 10, 0 + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 25) *100) ), } ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(25) + ) + ) end, { min_api_version = 17 @@ -420,41 +394,33 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus partially open following lift position update", function() - test.socket.capability:__set_channel_ordering("relaxed") + "WindowCovering CurrentPositionTiltPercent100ths triggers appropriate capability emission", function() test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 25) *100) + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 10000 ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(25) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) ) ) test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 0 + ), } ) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "WindowCovering OperationalStatus partially open following tilt position update", function() - test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + ) + ) test.socket.matter:__queue_receive( { mock_device.id, @@ -468,17 +434,38 @@ test.register_coroutine_test( "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(15) ) ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, ((100 - 65) *100) + ), + } + ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(65) ) ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowCovering TargetPositionLiftPercent100ths sets field appropriately", + function() test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + WindowCovering.attributes.TargetPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 5000 + ), } ) + test.wait_for_events() + assert(mock_device:get_field("__target_lift_percent") == 50) end, { min_api_version = 17 @@ -486,47 +473,59 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowCovering OperationalStatus partially open before lift position event", function() - test.socket.capability:__set_channel_ordering("relaxed") + "WindowCovering TargetPositionTiltPercent100ths sets field appropriately", + function() test.socket.matter:__queue_receive( { mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), + WindowCovering.attributes.TargetPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 5000 + ), } ) + test.wait_for_events() + assert(mock_device:get_field("__target_tilt_percent") == 50) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "WindowCovering TargetPositionLiftPercent100ths triggers operational status update when appropriate", + function() test.socket.matter:__queue_receive( { mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 25) *100) + mock_device, 10, ((100 - 65) *100) ), } ) test.socket.capability:__expect_send( mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(25) + "main", capabilities.windowShadeLevel.shadeLevel(65) ) ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_device, 10, 0 + ), + } + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.windowShade.windowShade.partially_open() ) ) - end, - { - min_api_version = 17 - } + end ) test.register_coroutine_test( - "WindowCovering OperationalStatus partially open before tilt position event", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), - } - ) + "WindowCovering TargetPositionTiltPercent100ths triggers operational status update when appropriate", + function() test.socket.matter:__queue_receive( { mock_device.id, @@ -540,6 +539,14 @@ test.register_coroutine_test( "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(65) ) ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_device, 10, 0 + ), + } + ) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.windowShade.windowShade.partially_open() @@ -551,129 +558,51 @@ test.register_coroutine_test( } ) -test.register_coroutine_test("WindowCovering OperationalStatus opening", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 25) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(25) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 1), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.opening() - ) - ) -end, -{ - min_api_version = 17 -} -) - -test.register_coroutine_test("WindowCovering OperationalStatus closing", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 25) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(25) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() +test.register_coroutine_test( + "Open capability command triggers appropriate Matter command", function() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShade", component = "main", command = "open", args = {}}, + } ) - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 2), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.closing() + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.UpOrOpen(mock_device, 10)} ) - ) -end, -{ - min_api_version = 17 -} + end, + { + min_api_version = 17 + } ) -test.register_coroutine_test("WindowCovering OperationalStatus unknown", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 25) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(25) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() +test.register_coroutine_test( + "Close capability command triggers appropriate Matter command", function() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = "windowShade", component = "main", command = "close", args = {}}, + } ) - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 3), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.unknown() + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.DownOrClose(mock_device, 10)} ) - ) -end, -{ - min_api_version = 17 -} + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( - "WindowShade open cmd handler", function() + "Pause capability command triggers appropriate Matter command", function() test.socket.capability:__queue_receive( { mock_device.id, - {capability = "windowShade", component = "main", command = "open", args = {}}, + {capability = "windowShade", component = "main", command = "pause", args = {}}, } ) test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.UpOrOpen(mock_device, 10)} + {mock_device.id, WindowCovering.server.commands.StopMotion(mock_device, 10)} ) - test.wait_for_events() end, { min_api_version = 17 @@ -681,17 +610,53 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowShade close cmd handler", function() + "Cached target lift/tilt position timeouts trigger one operational status update", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 50 * 100 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(50) + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) test.socket.capability:__queue_receive( { mock_device.id, - {capability = "windowShade", component = "main", command = "close", args = {}}, + {capability = "windowShade", component = "main", command = "open", args = {}}, } ) + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.DownOrClose(mock_device, 10)} + {mock_device.id, WindowCovering.server.commands.UpOrOpen(mock_device, 10)} ) + test.wait_for_events() + test.mock_time.advance_time(120) + + test.socket.capability:__expect_send({ + mock_device.id, { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } }, + } + ) end, { min_api_version = 17 @@ -699,17 +664,79 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "WindowShade pause cmd handler", function() + "Cached target lift timeout triggers one operational status update if tilt target and idle state are first reached appropriately", + function() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(50) + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( + mock_device, 10, 5000 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeLevel.shadeLevel(50) + ) + ) test.socket.capability:__queue_receive( { mock_device.id, - {capability = "windowShade", component = "main", command = "pause", args = {}}, + {capability = "windowShade", component = "main", command = "open", args = {}}, + } + ) + -- two timers are made when open is called + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.timer.__create_and_queue_test_time_advance_timer(120, "oneshot") + test.socket.matter:__expect_send( + {mock_device.id, WindowCovering.server.commands.UpOrOpen(mock_device, 10)} + ) + + -- ensure target state is cached before queueing responses + test.wait_for_events() + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( + mock_device, 10, 0 + ), + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) + ) + ) + test.socket.matter:__queue_receive( + { + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_device, 10, 0 + ), } ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.StopMotion(mock_device, 10)} - ) + + -- advance time to trigger the single remaining timer test.wait_for_events() + test.mock_time.advance_time(120) + test.socket.capability:__expect_send({ + mock_device.id, { capability_id = "windowShade", component_id = "main", attribute_id = "windowShade", state = { value = "partially open" } }, + } + ) + end, { min_api_version = 17 @@ -748,7 +775,7 @@ test.register_coroutine_test( } ) -test.register_coroutine_test("WindowShade setShadeLevel cmd handler", function() +test.register_coroutine_test("SetShadeLevel capability command triggers appropriate Matter command", function() test.socket.capability:__queue_receive( { mock_device.id, @@ -764,7 +791,7 @@ end, } ) -test.register_coroutine_test("WindowShade setShadeTiltLevel cmd handler", function() +test.register_coroutine_test("SetShadeTiltLevel capability command triggers appropriate Matter command", function() test.socket.capability:__queue_receive( { mock_device.id, @@ -780,24 +807,6 @@ end, } ) -test.register_coroutine_test("LevelControl CurrentLevel handler", function() - test.socket.matter:__queue_receive( - { - mock_device.id, - clusters.LevelControl.attributes.CurrentLevel:build_test_report_data(mock_device, 10, 100), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(math.floor((100 / 254.0 * 100) + .5)) - ) - ) -end, -{ - min_api_version = 17 -} -) - --test battery test.register_coroutine_test( "Battery percent reports should generate correct messages", function() @@ -820,29 +829,6 @@ test.register_coroutine_test( } ) -test.register_coroutine_test("OperationalStatus report contains current position report", function() - test.socket.capability:__set_channel_ordering("relaxed") - local report = WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 25) *100) - ) - table.insert(report.info_blocks, WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0).info_blocks[1]) - test.socket.matter:__queue_receive({ mock_device.id, report}) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(25) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) -end, -{ - min_api_version = 17 -} -) - test.register_coroutine_test( "Handle preset commands", function() @@ -911,9 +897,9 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "InfoChanged event checks for new profile match if device has changed (i.e. through reinterview or SW update)", + "InfoChanged event updates new profile if sw updated has occurred", function() - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({})) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ matter_version = {software = 2} })) local read_attribute_list = clusters.PowerSource.attributes.AttributeList:read() test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) test.wait_for_events() @@ -925,198 +911,10 @@ test.register_coroutine_test( } ) -test.register_coroutine_test( - "WindowCovering shade level adjusted by greater than 2%; status reflects Closing followed by Partially Open", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), - } - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 25) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(25) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) - test.wait_for_events() - test.socket.capability:__queue_receive( - { - mock_device.id, - {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 19 }}, - } - ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 8100)} - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 10), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.closing() - ) - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 23) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(23) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 21) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(21) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 19) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(19) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) - end, - { - min_api_version = 17 - } -) - -test.register_coroutine_test( - "WindowCovering shade level adjusted by less than or equal to 2%; status reflects Closing followed by Partially Open", function() - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 0), - } - ) - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 25) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(25) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) - test.wait_for_events() - test.socket.capability:__queue_receive( - { - mock_device.id, - {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 23 }}, - } - ) - test.socket.matter:__expect_send( - {mock_device.id, WindowCovering.server.commands.GoToLiftPercentage(mock_device, 10, 7700)} - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.OperationalStatus:build_test_report_data(mock_device, 10, 10), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.closing() - ) - ) - test.wait_for_events() - test.socket.matter:__queue_receive( - { - mock_device.id, - WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( - mock_device, 10, ((100 - 23) *100) - ), - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShadeLevel.shadeLevel(23) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) - end, - { - min_api_version = 17 - } -) - test.register_coroutine_test( "Check that preference updates to reverse polarity after being set to true and that the shade lift operates as expected when opening and closing", function() test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = { reverse = "true" } })) test.wait_for_events() - local reverse_preference_set = mock_device:get_field("__reverse_polarity") - assert(reverse_preference_set == true, "reverse_preference_set is True") test.socket.matter:__queue_receive({ mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( @@ -1128,11 +926,19 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(0) ) ) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_device, 10, 0 + ) + }) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.windowShade.windowShade.open() ) ) + test.wait_for_events() test.socket.matter:__queue_receive({ mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercent100ths:build_test_report_data( @@ -1144,11 +950,19 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(100) ) ) + test.wait_for_events() + test.socket.matter:__queue_receive({ + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_device, 10, 0 + ) + }) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.windowShade.windowShade.closed() ) ) + test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, {capability = "windowShadeLevel", component = "main", command = "setShadeLevel", args = { 85 }}, @@ -1173,8 +987,6 @@ test.register_coroutine_test( "Check that preference updates to reverse polarity after being set to true and that the shade tilt operates as expected when opening and closing", function() test.socket.device_lifecycle():__queue_receive(mock_device:generate_info_changed({ preferences = { reverse = "true" } })) test.wait_for_events() - local reverse_preference_set = mock_device:get_field("__reverse_polarity") - assert(reverse_preference_set == true, "reverse_preference_set is True") test.socket.matter:__queue_receive({ mock_device.id, WindowCovering.attributes.CurrentPositionTiltPercent100ths:build_test_report_data( @@ -1186,6 +998,12 @@ test.register_coroutine_test( "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(0) ) ) + test.socket.matter:__queue_receive({ + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_device, 10, 0 + ) + }) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.windowShade.windowShade.open() @@ -1197,6 +1015,12 @@ test.register_coroutine_test( mock_device, 10, 0 ) }) + test.socket.matter:__queue_receive({ + mock_device.id, + WindowCovering.attributes.OperationalStatus:build_test_report_data( + mock_device, 10, 0 + ) + }) test.socket.capability:__expect_send( mock_device:generate_test_message( "main", capabilities.windowShadeTiltLevel.shadeTiltLevel(100) @@ -1207,6 +1031,7 @@ test.register_coroutine_test( "main", capabilities.windowShade.windowShade.closed() ) ) + test.wait_for_events() test.socket.capability:__queue_receive({ mock_device.id, {capability = "windowShadeTiltLevel", component = "main", command = "setShadeTiltLevel", args = { 15 }}, @@ -1261,11 +1086,6 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(50) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -1297,11 +1117,6 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(30) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -1343,11 +1158,6 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(50) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -1398,11 +1208,6 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(95) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) test.wait_for_events() test.socket.capability:__queue_receive( { @@ -1434,11 +1239,6 @@ test.register_coroutine_test( "main", capabilities.windowShadeLevel.shadeLevel(5) ) ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", capabilities.windowShade.windowShade.partially_open() - ) - ) test.wait_for_events() test.socket.capability:__queue_receive( { From 010ab9a4aa7fd8776e37eaffb268e17b176fa970 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Mon, 27 Jul 2026 13:53:03 -0500 Subject: [PATCH 074/102] GHA: Check for category existence on the main component (#3136) --- .github/scripts/check_duplicates.py | 89 +++++++++---------- .github/scripts/check_profile_categories.py | 82 +++++++++++++++++ .../check-profile-categories-comment.yml | 34 +++++++ .../workflows/check-profile-categories.yml | 41 +++++++++ .github/workflows/duplicate-profiles.yml | 17 ++-- 5 files changed, 205 insertions(+), 58 deletions(-) create mode 100644 .github/scripts/check_profile_categories.py create mode 100644 .github/workflows/check-profile-categories-comment.yml create mode 100644 .github/workflows/check-profile-categories.yml diff --git a/.github/scripts/check_duplicates.py b/.github/scripts/check_duplicates.py index 65e2ae2ea6..e335f6083a 100644 --- a/.github/scripts/check_duplicates.py +++ b/.github/scripts/check_duplicates.py @@ -1,4 +1,3 @@ -import csv import os import yaml from pathlib import Path @@ -101,51 +100,49 @@ def compare_components(prof1, prof2): return True -with open(str(Path.home()) + '/files.csv', 'r') as csvfile: - csvreader = csv.reader(csvfile) - changed_files = next(csvreader) - - for file in changed_files: - file_basename = os.path.basename(file) - file_directory = os.path.dirname(file) - - if '/profiles/' in file: - print('\nNEW PROFILE:\n%s is a profile! Comparing to other profiles...' % file) - - os.chdir(file_directory) - new_profile = file_basename - - # Skip deleted files and track them for warning - if not os.path.exists(new_profile): - print("Skipping %s - file was deleted" % new_profile) - deleted_profiles.append(file) - os.chdir(cwd) - continue - - for current_profile in os.listdir("./"): - # compare to YAML files that are not the same file - # Compare only .yml files and only files that have not already been found to be a duplicate - if current_profile != new_profile and Path(current_profile).suffix == ".yml" and (current_profile, new_profile) not in duplicate_pairs: - print("Comparing %s vs %s" % (new_profile, current_profile)) - with open(new_profile) as new_data, open(current_profile) as current_data: - new_profile_map = yaml.safe_load(new_data) - current_profile_map = yaml.safe_load(current_data) - - ''' Compare profiles. A duplicate is defined as follows: - - categories must be the same - - capabilities must be the same, with some ordering restrictions - - top capability must match, but subsequent ordering does not matter - - embedded configs must be the same, but certain values can be ordered differently (i.e. enabledValues) - - preferences must be the same - ''' - if(compare_preferences(new_profile_map, current_profile_map) == True and - compare_metadata(new_profile_map, current_profile_map) == True and - compare_components(new_profile_map, current_profile_map) == True): - print("%s and %s are duplicates!\n" % (new_profile, current_profile)) - duplicate_pairs.append((new_profile, current_profile)) - - # return to original directory - os.chdir(cwd) +changed_files = os.environ.get('ALL_CHANGED_FILES', '').split() + +for file in changed_files: + file_basename = os.path.basename(file) + file_directory = os.path.dirname(file) + + if '/profiles/' in file: + print('\nNEW PROFILE:\n%s is a profile! Comparing to other profiles...' % file) + + os.chdir(file_directory) + new_profile = file_basename + + # Skip deleted files and track them for warning + if not os.path.exists(new_profile): + print("Skipping %s - file was deleted" % new_profile) + deleted_profiles.append(file) + os.chdir(cwd) + continue + + for current_profile in os.listdir("./"): + # compare to YAML files that are not the same file + # Compare only .yml files and only files that have not already been found to be a duplicate + if current_profile != new_profile and Path(current_profile).suffix == ".yml" and (current_profile, new_profile) not in duplicate_pairs: + print("Comparing %s vs %s" % (new_profile, current_profile)) + with open(new_profile) as new_data, open(current_profile) as current_data: + new_profile_map = yaml.safe_load(new_data) + current_profile_map = yaml.safe_load(current_data) + + ''' Compare profiles. A duplicate is defined as follows: + - categories must be the same + - capabilities must be the same, with some ordering restrictions + - top capability must match, but subsequent ordering does not matter + - embedded configs must be the same, but certain values can be ordered differently (i.e. enabledValues) + - preferences must be the same + ''' + if(compare_preferences(new_profile_map, current_profile_map) == True and + compare_metadata(new_profile_map, current_profile_map) == True and + compare_components(new_profile_map, current_profile_map) == True): + print("%s and %s are duplicates!\n" % (new_profile, current_profile)) + duplicate_pairs.append((new_profile, current_profile)) + + # return to original directory + os.chdir(cwd) with open("profile-comment-body.md", "w") as f: if duplicate_pairs: diff --git a/.github/scripts/check_profile_categories.py b/.github/scripts/check_profile_categories.py new file mode 100644 index 0000000000..31609cbe7e --- /dev/null +++ b/.github/scripts/check_profile_categories.py @@ -0,0 +1,82 @@ +import os +import sys +import yaml +from pathlib import Path + +cwd = os.getcwd() +missing_category_profiles = [] +deleted_profiles = [] + +changed_files = os.environ.get('ALL_CHANGED_FILES', '').split() + +for file in changed_files: + file_basename = os.path.basename(file) + file_directory = os.path.dirname(file) + + if '/profiles/' in file and file.endswith('.yml'): + print('\nCHECKING PROFILE:\n%s' % file) + + os.chdir(file_directory) + + if not os.path.exists(file_basename): + print("Skipping %s - file was deleted" % file_basename) + deleted_profiles.append(file) + os.chdir(cwd) + continue + + with open(file_basename) as fp: + try: + profile = yaml.safe_load(fp) + except yaml.YAMLError as e: + print("Error parsing %s: %s" % (file_basename, e)) + os.chdir(cwd) + continue + + if not profile or 'components' not in profile: + print("Skipping %s - no components found" % file_basename) + os.chdir(cwd) + continue + + # Find the main component and verify it has a categories field + main_component = next( + (c for c in profile['components'] if c.get('id') == 'main'), + None + ) + + if main_component is None: + print("Warning: %s has no 'main' component" % file_basename) + os.chdir(cwd) + continue + + if not main_component.get('categories'): + print("MISSING CATEGORY: %s" % file) + missing_category_profiles.append(file) + else: + print("OK: %s has categories: %s" % ( + file_basename, + [c['name'] for c in main_component['categories']] + )) + + os.chdir(cwd) + +with open("profile-categories-comment-body.md", "w") as f: + if missing_category_profiles: + f.write("Profile category check: :x: **Missing categories detected.**\n\n") + f.write("The following profiles are missing a `categories` field on the `main` component:\n\n") + for profile in missing_category_profiles: + f.write("- `%s`\n" % profile) + f.write("\nPlease add a `categories` entry to the `main` component. Example:\n") + f.write("```yaml\ncomponents:\n - id: main\n categories:\n - name: Switch\n capabilities:\n ...\n```\n") + else: + f.write("Profile category check: :white_check_mark: Passed - all profiles have a category defined.\n") + + if deleted_profiles: + f.write("\n:warning: **Deleted profile files detected:**\n") + for deleted in deleted_profiles: + f.write("- `%s`\n" % deleted) + +with open("profile-categories-comment-body.md", "r") as f: + print("\n" + f.read()) + +if missing_category_profiles: + sys.exit(1) diff --git a/.github/workflows/check-profile-categories-comment.yml b/.github/workflows/check-profile-categories-comment.yml new file mode 100644 index 0000000000..cf207144ef --- /dev/null +++ b/.github/workflows/check-profile-categories-comment.yml @@ -0,0 +1,34 @@ +name: Post profile categories comment +on: + workflow_run: + workflows: [Check profile categories] + types: + - completed + +jobs: + comment-on-pr: + runs-on: ubuntu-latest + steps: + - name: Download comment artifact + uses: dawidd6/action-download-artifact@v6 + with: + workflow: check-profile-categories.yml + run_id: ${{ github.event.workflow_run.id }} + + - run: echo "pr_number=$(cat pr_number/pr_number.txt)" >> $GITHUB_ENV + + - name: Find comment + uses: peter-evans/find-comment@v2 + id: fc + with: + body-includes: Profile category check + comment-author: 'github-actions[bot]' + issue-number: ${{ env.pr_number }} + + - name: Post comment + uses: peter-evans/create-or-update-comment@v2 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + body-file: 'profile_categories_comment/profile-categories-comment-body.md' + edit-mode: replace + issue-number: ${{ env.pr_number }} diff --git a/.github/workflows/check-profile-categories.yml b/.github/workflows/check-profile-categories.yml new file mode 100644 index 0000000000..6340175810 --- /dev/null +++ b/.github/workflows/check-profile-categories.yml @@ -0,0 +1,41 @@ +name: Check profile categories +on: + pull_request: + types: [opened, synchronize] + paths: + - 'drivers/**/profiles/*.yml' + +jobs: + check-profile-categories: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Gather file changes + id: changed-files + uses: tj-actions/changed-files@v47 + + - name: Run python script + env: + ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} + run: | + python ./.github/scripts/check_profile_categories.py + + - name: Upload profile categories comment artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: profile_categories_comment + path: | + profile-categories-comment-body.md + + - run: echo ${{ github.event.number }} > pr_number.txt + + - name: Upload PR info + if: always() + uses: actions/upload-artifact@v4 + with: + name: pr_number + path: | + pr_number.txt diff --git a/.github/workflows/duplicate-profiles.yml b/.github/workflows/duplicate-profiles.yml index ae524cc1c2..5f66269cc1 100644 --- a/.github/workflows/duplicate-profiles.yml +++ b/.github/workflows/duplicate-profiles.yml @@ -13,20 +13,13 @@ jobs: - name: Checkout uses: actions/checkout@v3 - # Creates file "$/files.csv", among others - - id: file_changes - name: Gather file changes - uses: trilom/file-changes-action@1.2.4 - with: - output: ',' - fileOutput: ',' - - # For verification - - name: Show files changed - run: | - cat $HOME/files.csv + - name: Gather file changes + id: changed-files + uses: tj-actions/changed-files@v47 - name: Run python script + env: + ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} run: | python ./.github/scripts/check_duplicates.py From fdd699a5b32dfea93587b998868ff34887d1bfdb Mon Sep 17 00:00:00 2001 From: SiHAS <71238736+shinasys@users.noreply.github.com> Date: Wed, 29 Jul 2026 03:50:06 +0900 Subject: [PATCH 075/102] Add shinasystem's SiHAS Smoke Detector for wwst (#3134) * Add shinasystem's SiHAS Smoke Detector for wwst * Remove unused variable capabilities * update year --- .../zigbee-smoke-detector/fingerprints.yml | 5 +++ .../src/shinasystem/can_handle.lua | 11 ++++++ .../src/shinasystem/init.lua | 34 +++++++++++++++++++ .../zigbee-smoke-detector/src/sub_drivers.lua | 1 + 4 files changed, 51 insertions(+) create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/shinasystem/can_handle.lua create mode 100644 drivers/SmartThings/zigbee-smoke-detector/src/shinasystem/init.lua diff --git a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml index c1f859037b..6fce8d0d3a 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml +++ b/drivers/SmartThings/zigbee-smoke-detector/fingerprints.yml @@ -59,3 +59,8 @@ zigbeeManufacturer: manufacturer: MultIR model: MIR-SM200 deviceProfileName: smoke-battery-tamper-no-fw-update + - id: ShinaSystem/FAM-300Z + deviceLabel: SiHAS Smoke Detector + manufacturer: ShinaSystem + model: FAM-300Z + deviceProfileName: smoke-battery diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/shinasystem/can_handle.lua b/drivers/SmartThings/zigbee-smoke-detector/src/shinasystem/can_handle.lua new file mode 100644 index 0000000000..c13aebce6a --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/shinasystem/can_handle.lua @@ -0,0 +1,11 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local function shinasystem_can_handle(opts, driver, device, ...) + if device:get_manufacturer() == "ShinaSystem" and (device:get_model() == "FAM-300Z") then + return true, require("shinasystem") + end + return false +end + +return shinasystem_can_handle diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/shinasystem/init.lua b/drivers/SmartThings/zigbee-smoke-detector/src/shinasystem/init.lua new file mode 100644 index 0000000000..19e59e4aaf --- /dev/null +++ b/drivers/SmartThings/zigbee-smoke-detector/src/shinasystem/init.lua @@ -0,0 +1,34 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + + +local zcl_clusters = require "st.zigbee.zcl.clusters" +local IASZone = zcl_clusters.IASZone + +local CONFIGURATIONS = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 1, + maximum_interval = 1200, -- Zigbee poll interval is 600s. Added because the default reporting maximum_interval (180s) must be greater than 600s. + data_type = IASZone.attributes.ZoneStatus.base_type, + reportable_change = 1 + } +} + +local function device_init(driver, device) + if CONFIGURATIONS ~= nil then + for _, attribute in ipairs(CONFIGURATIONS) do + device:add_configured_attribute(attribute) + end + end +end + +local shinasystem_smoke_sensor = { + NAME = "shinasystem smoke sensor", + lifecycle_handlers = { + init = device_init + }, + can_handle = require("shinasystem.can_handle"), +} +return shinasystem_smoke_sensor diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua b/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua index 2b917b85dc..2f7a4b29ff 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/sub_drivers.lua @@ -7,5 +7,6 @@ local sub_drivers = { lazy_load_if_possible("aqara-gas"), lazy_load_if_possible("aqara"), lazy_load_if_possible("MultiIR"), + lazy_load_if_possible("shinasystem"), } return sub_drivers From 4026115e52a2ceb27b03dde8c31fe3e14480cb00 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Fri, 24 Jul 2026 14:47:12 -0500 Subject: [PATCH 076/102] Branching registryCredentialsId and registryURL based off region and nodeLabel --- Jenkinsfile | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 425da986db..a7a143ae64 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -24,11 +24,42 @@ def getChangedDrivers() { return drivers } +def get_region() { + def uri = new URI(env.JENKINS_URL) + def region = uri.host.endsWith('.cn') ? 'cn' : 'global' + return region +} + +// Gate artifactory-credentials for prod nodes in cn due to a docker authentication error arising in that environment +def getDockerCredentialId() { + def nodeLabel = params.NODE_LABEL ?: 'production' + def region = get_region() + if (nodeLabel == 'production' && region == 'cn') { + return 'artifactory-credentials' + } + else { + return 'artifactory' + } +} +// Gate RegistryUrl for prod nodes in cn due to a docker authentication error arising in that environment +def getRegistryUrl() { + def nodeLabel = params.NODE_LABEL ?: 'production' + def region = get_region() + if (nodeLabel == 'production' && region == 'cn') { + return 'https://registry.artifactoryedge.streleng.cn' + } else { + // Default + return '' + } +} + pipeline { agent { docker { image 'python:3.10' label "${params.NODE_LABEL ?: 'production'}" + registryUrl getRegistryUrl() + registryCredentialsId getDockerCredentialId() args '--entrypoint= -u 0:0' } } From 3083caa47e7741b04eaa44ce2050845c4b866c37 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 29 Jul 2026 11:24:20 -0500 Subject: [PATCH 077/102] Avoiding URI permissions error and defaulted credentials ID string to empty --- Jenkinsfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a7a143ae64..752d3d358c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -25,8 +25,12 @@ def getChangedDrivers() { } def get_region() { - def uri = new URI(env.JENKINS_URL) - def region = uri.host.endsWith('.cn') ? 'cn' : 'global' + def url = env.JENKINS_URL?.trim() + if (url?.endsWith('/')) { + url = url[0..-2] + } + + def region = url?.endsWith('.cn') ? 'cn' : 'global' return region } @@ -38,7 +42,7 @@ def getDockerCredentialId() { return 'artifactory-credentials' } else { - return 'artifactory' + return '' } } // Gate RegistryUrl for prod nodes in cn due to a docker authentication error arising in that environment From eb505a9268c1835a5c9d8a378247c6ba5096a52b Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 30 Jul 2026 12:44:29 -0500 Subject: [PATCH 078/102] WWSTCERT-12681 Lockly Smart Lock (#3114) * WWSTCERT-12681 Lockly Smart Look * Update fingerprints.yml * Update fingerprints.yml --- drivers/SmartThings/matter-lock/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 75f2fcf37d..7eb674bce9 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -197,6 +197,12 @@ matterManufacturer: vendorId: 0x152C productId: 0x9501 deviceProfileName: lock + #Lockly + - id: "5212/2" + deviceLabel: Lockly Smart Lock + vendorId: 0x145C + productId: 0x0002 + deviceProfileName: lock-modular matterGeneric: - id: "matter/door-lock" deviceLabel: Matter Door Lock From 2bd5faa6f70203898709d5c323730121e41a2637 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:04:51 -0500 Subject: [PATCH 079/102] Matter Switch: Fix Electrical profiling coroutine errors and improve fallback handling (#3150) --- .../types/EnergyMeasurementStruct.lua | 24 +++++++++++++++---- .../SmartThings/matter-switch/src/init.lua | 5 ++++ .../switch_handlers/attribute_handlers.lua | 5 ++++ .../src/switch_utils/device_configuration.lua | 9 ++++++- .../matter-switch/src/switch_utils/utils.lua | 20 ++++++++++++---- .../src/test/test_aqara_light_switch_h2.lua | 5 ++++ .../src/test/test_electrical_sensor_set.lua | 5 ++++ .../src/test/test_electrical_sensor_tree.lua | 5 ++++ 8 files changed, 68 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua index a4c58a3646..5452b287ac 100644 --- a/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua +++ b/drivers/SmartThings/matter-switch/src/embedded_clusters/ElectricalEnergyMeasurement/types/EnergyMeasurementStruct.lua @@ -42,6 +42,20 @@ EnergyMeasurementStruct.field_defs = { is_optional = true, data_type = require "st.matter.data_types.Uint64", }, + { + name = "apparent_energy", + field_id = 5, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Int64", + }, + { + name = "reactive_energy", + field_id = 6, + is_nullable = false, + is_optional = true, + data_type = require "st.matter.data_types.Int64", + }, } EnergyMeasurementStruct.init = function(cls, tbl) @@ -49,10 +63,10 @@ EnergyMeasurementStruct.init = function(cls, tbl) o.elements = {} o.num_elements = 0 setmetatable(o, new_mt) - for idx, field_def in ipairs(cls.field_defs) do + for _idx, field_def in ipairs(cls.field_defs) do 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) - else + 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 @@ -71,7 +85,7 @@ new_mt.__index.serialize = EnergyMeasurementStruct.serialize EnergyMeasurementStruct.augment_type = function(self, val) local elems = {} local num_elements = 0 - for _, v in pairs(val.elements) do + for _, v in pairs(val.elements or {}) do for _, field_def in ipairs(self.field_defs) do if field_def.field_id == v.field_id and field_def.is_nullable and @@ -79,11 +93,11 @@ EnergyMeasurementStruct.augment_type = function(self, val) 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) then + (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 + for i, e in ipairs(elems[field_def.name].elements or {}) do elems[field_def.name].elements[i] = data_types.validate_or_build_type(e, field_def.element_type) end end diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 9d4e22fb15..adebbb0ec1 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -29,6 +29,11 @@ if version.api < 16 then clusters.Descriptor = require "embedded_clusters.Descriptor" end +-- Catch nil elements errors gracefully without receiving a coroutine error +if version.api < 21 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct" +end + local SwitchLifecycleHandlers = {} function SwitchLifecycleHandlers.device_added(driver, device) 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..753c76992e 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -18,6 +18,11 @@ if version.api < 11 then clusters.PowerTopology = require "embedded_clusters.PowerTopology" end +-- Catch nil elements errors gracefully without receiving a coroutine error +if version.api < 21 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct" +end + local AttributeHandlers = {} -- [[ ON OFF CLUSTER ATTRIBUTES ]] -- 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..130fd1c1e4 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -15,6 +15,11 @@ if version.api < 11 then clusters.ValveConfigurationAndControl = require "embedded_clusters.ValveConfigurationAndControl" end +-- Catch nil elements errors gracefully without receiving a coroutine error +if version.api < 21 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct" +end + local DeviceConfiguration = {} local ChildConfiguration = {} local SwitchDeviceConfiguration = {} @@ -87,7 +92,9 @@ function SwitchDeviceConfiguration.assign_profile_for_onoff_ep(device, server_on local generic_profile = fields.device_type_profile_map[primary_dt_id] local static_electrical_tags = switch_utils.get_field_for_endpoint(device, fields.ELECTRICAL_TAGS, server_onoff_ep_id) - if static_electrical_tags ~= nil then + if type(static_electrical_tags) == "string" then + -- if no associated profile is found for the device type and static electrical tags are available, use "plug-binary" as a fallback + generic_profile = generic_profile or "plug-binary" -- profiles like 'light-binary' and 'plug-binary' should drop the '-binary' and become 'light-power', 'plug-energy-powerConsumption', etc. generic_profile = string.gsub(generic_profile, "-binary", "") .. static_electrical_tags end diff --git a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua index d2457bfb4d..1d12f41781 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/utils.lua @@ -21,6 +21,11 @@ if version.api < 16 then clusters.Descriptor = require "embedded_clusters.Descriptor" end +-- Catch nil elements errors gracefully without receiving a coroutine error +if version.api < 21 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct" +end + local utils = {} function utils.tbl_contains(array, value) @@ -545,16 +550,14 @@ function utils.subscribe(device) devices_seen[checked_device.id] = true -- only loop through any device once end end - -- The refresh capability command handler in the lua libs uses this key to determine which attributes to read. Note - -- that only attributes_seen needs to be saved here, and not events_seen, since the refresh handler only checks - -- attributes and not events. - device:set_field(fields.SUBSCRIBED_ATTRIBUTES_KEY, attributes_seen) -- If the type of battery support has not yet been determined, add the PowerSource AttributeList to the list of -- subscribed attributes in order to determine which if any battery capability should be used. if device:get_field(fields.profiling_data.BATTERY_SUPPORT) == nil then local ib = im.InteractionInfoBlock(nil, clusters.PowerSource.ID, clusters.PowerSource.attributes.AttributeList.ID) subscribe_request:with_info_block(ib) + attributes_seen[clusters.PowerSource.ID] = attributes_seen[clusters.PowerSource.ID] or {} + attributes_seen[clusters.PowerSource.ID][clusters.PowerSource.attributes.AttributeList.ID] = ib end -- If the power topology of the device has not yet been determined, add the AvailableEndpoints (for SET topology) @@ -566,12 +569,21 @@ function utils.subscribe(device) if clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.SET_TOPOLOGY, endpoint_power_topology_cluster.feature_map or 0) then local ib = im.InteractionInfoBlock(nil, clusters.PowerTopology.ID, clusters.PowerTopology.attributes.AvailableEndpoints.ID) subscribe_request:with_info_block(ib) + attributes_seen[clusters.PowerTopology.ID] = attributes_seen[clusters.PowerTopology.ID] or {} + attributes_seen[clusters.PowerTopology.ID][clusters.PowerTopology.attributes.AvailableEndpoints.ID] = ib elseif clusters.PowerTopology.are_features_supported(clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, endpoint_power_topology_cluster.feature_map or 0) then local ib = im.InteractionInfoBlock(nil, clusters.Descriptor.ID, clusters.Descriptor.attributes.PartsList.ID) subscribe_request:with_info_block(ib) + attributes_seen[clusters.Descriptor.ID] = attributes_seen[clusters.Descriptor.ID] or {} + attributes_seen[clusters.Descriptor.ID][clusters.Descriptor.attributes.PartsList.ID] = ib end end + -- The refresh capability command handler in the lua libs uses this key to determine which attributes to read. Note + -- that only attributes_seen needs to be saved here, and not events_seen, since the refresh handler only checks + -- attributes and not events. + device:set_field(fields.SUBSCRIBED_ATTRIBUTES_KEY, attributes_seen) + if #subscribe_request.info_blocks > 0 then device:send(subscribe_request) end diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua index f763cc769f..468232cbc3 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua @@ -15,6 +15,11 @@ if version.api < 11 then clusters.PowerTopology = require "embedded_clusters.PowerTopology" end +-- Catch nil elements errors gracefully without receiving a coroutine error +if version.api < 21 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct" +end + local aqara_parent_ep = 4 local aqara_child1_ep = 1 local aqara_child2_ep = 2 diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua index dae6b09a6c..18c44f7b9c 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua @@ -15,6 +15,11 @@ if version.api < 11 then clusters.PowerTopology = require "embedded_clusters.PowerTopology" end +-- Catch nil elements errors gracefully without receiving a coroutine error +if version.api < 21 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct" +end + local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("plug-level-power-energy-powerConsumption.yml"), manufacturer_info = { diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua index d691f2af27..2abfe6e9c5 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua @@ -19,6 +19,11 @@ if version.api < 16 then clusters.Descriptor = require "embedded_clusters.Descriptor" end +-- Catch nil elements errors gracefully without receiving a coroutine error +if version.api < 21 then + clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct = require "embedded_clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct" +end + local mock_device = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("plug-level-power-energy-powerConsumption.yml"), manufacturer_info = { From 1451f18aaf00bc74f1c013260185a1f24f10c498 Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Wed, 29 Jul 2026 10:35:07 -0500 Subject: [PATCH 080/102] Grab latest capability definitions from API for tests --- .github/workflows/run-tests.yml | 24 ++ tools/fetch_capability_definitions.py | 583 ++++++++++++++++++++++++++ tools/run_driver_tests.py | 8 +- tools/run_driver_tests_p.py | 8 +- 4 files changed, 619 insertions(+), 4 deletions(-) create mode 100644 tools/fetch_capability_definitions.py diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f6a03154cb..de88fc02d1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -4,6 +4,8 @@ on: types: [opened, synchronize, labeled, unlabeled] paths: - 'drivers/**' + - 'tools/run_driver_tests.py' + - 'tools/run_driver_tests_p.py' jobs: # Two separate jobs for finding the right artifact to run tests with @@ -91,11 +93,33 @@ jobs: - run: echo ${{ steps.changed-drivers.outputs.all_modified_files }} - name: Install Python requirements run: pip install -r tools/requirements.txt + - name: Fetch capability definitions + continue-on-error: true + run: | + python3 tools/fetch_capability_definitions.py \ + --drivers-dir ${{ github.workspace }}/drivers \ + --output-dir ${{ github.workspace }}/capability_json \ + --failed-output-file ${{ github.workspace }}/capability_failures_comment.md + env: + CAPABILITY_PAT: ${{ secrets.CAPABILITY_DEFINITIONS_PAT }} + - name: Report capability download failures + if: hashFiles('capability_failures_comment.md') != '' && always() + uses: marocchino/sticky-pull-request-comment@v2 + with: + path: ${{ github.workspace }}/capability_failures_comment.md + header: capability-download-failures + - name: Clear capability download failures comment + if: hashFiles('capability_failures_comment.md') == '' && always() + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: capability-download-failures + delete: true - name: Run the tests id: run-tests run: python tools/run_driver_tests_p.py ${{ steps.changed-drivers.outputs.all_modified_files }} env: LUA_PATH: ${{ steps.lua_path.outputs.lua_path }} + ST_CAPABILITY_JSON_DIR: ${{ github.workspace }}/capability_json - name: Upload test artifact if: always() uses: actions/upload-artifact@v4 diff --git a/tools/fetch_capability_definitions.py b/tools/fetch_capability_definitions.py new file mode 100644 index 0000000000..45ef47b9cd --- /dev/null +++ b/tools/fetch_capability_definitions.py @@ -0,0 +1,583 @@ +#!/usr/bin/env python3 +# Copyright 2026 SmartThings +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""fetch_capability_definitions.py + +Scans driver profile YAML files to discover all capability IDs and versions in +use, then downloads their fully-inlined JSON definitions from the SmartThings +cloud API and writes each to a separate file in the output directory. + +The downloaded JSON files are pre-inlined by the cloud (no $ref strings remain) +and can be used directly by the integration test framework's capability_json_loader +module via the ST_CAPABILITY_JSON_DIR environment variable. + +Capability definitions are fetched individually via GET /capabilities// +by default, which ensures that a failure for one capability does not affect others. +A known allowlist of capabilities (QUERY_ENDPOINT_IDS) that are confirmed to work +with the bulk POST /capabilities/query endpoint are fetched in a single batch +request instead, for efficiency. + +Usage +----- + python3 tools/fetch_capability_definitions.py \\ + --drivers-dir drivers/ \\ + --drivers-dir ~/projects/SmartThingsEdgeDrivers/drivers \\ + --output-dir ~/cap_cache \\ + [--overwrite] \\ + [--failed-output-file failures_comment.md] + +Arguments +--------- + --drivers-dir PATH Root of a driver tree to scan for profiles. + May be specified multiple times. + --output-dir PATH Directory to write _.json files. + Created if it does not exist. + --overwrite When set, re-download and overwrite all capability + files even if they already exist in --output-dir. + When absent (default), only missing files are fetched; + capabilities already on disk are skipped entirely, + and no API request is made for them. + --failed-output-file PATH When set, write a markdown-formatted report of any + capabilities that could not be downloaded to this file. + The file is only created when there are failures, making + it suitable for use as a PR comment via hashFiles() checks + in CI workflows. + +Environment +----------- + CAPABILITY_PAT Required. SmartThings personal access token used for + the Authorization: Bearer header on all API requests. +""" + +import argparse +import json +import os +import sys +import time +from pathlib import Path +from typing import Dict, List, Set, Tuple + +import requests +import yaml + +API_BASE = "https://api.smartthings.com/v1" +QUERY_ENDPOINT = f"{API_BASE}/capabilities/query" + +# These two IDs appear in the /capabilities list but are rejected by both the +# /capabilities/query endpoint and the individual GET endpoint. They are +# lowercase/deprecated duplicates of alarmSensor and samsungTV respectively. +EXCLUDED_IDS = frozenset(["alarmsensor", "samsungTv"]) + +# These capability IDs are confirmed to work with the bulk POST /capabilities/query +# endpoint and will be fetched in a single batch request for efficiency. All other +# capability IDs (e.g. namespaced capabilities such as stse.*, sec.*, samsungim.*) +# are fetched individually via GET /capabilities// by default. This +# ensures that a new capability added to a profile that is not supported by the +# query endpoint will only cause an isolated failure rather than affecting all +# capabilities in the batch. +QUERY_ENDPOINT_IDS = frozenset([ + "accelerationSensor", + "activitySensor", + "airConditionerFanMode", + "airConditionerMode", + "airPurifierFanMode", + "airQualityHealthConcern", + "airQualitySensor", + "alarm", + "atmosphericPressureMeasurement", + "audioMute", + "audioNotification", + "audioRecording", + "audioTrackData", + "audioVolume", + "battery", + "batteryLevel", + "bodyWeightMeasurement", + "bridge", + "button", + "cameraPrivacyMode", + "cameraViewportSettings", + "carbonDioxideHealthConcern", + "carbonDioxideMeasurement", + "carbonMonoxideDetector", + "carbonMonoxideHealthConcern", + "carbonMonoxideMeasurement", + "chargingState", + "chime", + "colorControl", + "colorTemperature", + "configuration", + "contactSensor", + "cookTime", + "currentMeasurement", + "dewPoint", + "doorControl", + "dustHealthConcern", + "dustSensor", + "elevatorCall", + "energyMeter", + "evseChargingSession", + "evseState", + "fanMode", + "fanOscillationMode", + "fanSpeed", + "fanSpeedPercent", + "feederOperatingState", + "feederPortion", + "filterState", + "filterStatus", + "fineDustHealthConcern", + "fineDustSensor", + "firmwareUpdate", + "flowMeasurement", + "formaldehydeHealthConcern", + "formaldehydeMeasurement", + "gasDetector", + "gasMeter", + "hardwareFault", + "hdr", + "illuminanceMeasurement", + "imageCapture", + "imageControl", + "keypadInput", + "knob", + "laundryWasherRinseMode", + "laundryWasherSpinSpeed", + "level", + "localMediaStorage", + "lock", + "lockAlarm", + "lockAliro", + "lockCodes", + "lockCredentials", + "lockSchedules", + "lockUsers", + "mechanicalPanTiltZoom", + "mediaGroup", + "mediaPlayback", + "mediaPresets", + "mediaTrackControl", + "mode", + "moldHealthConcern", + "momentary", + "motionSensor", + "movementSensor", + "multipleZonePresence", + "nightVision", + "nitrogenDioxideHealthConcern", + "nitrogenDioxideMeasurement", + "operationalState", + "ozoneHealthConcern", + "ozoneMeasurement", + "panicAlarm", + "pestControl", + "powerConsumptionReport", + "powerMeter", + "powerSource", + "presenceSensor", + "pumpControlMode", + "pumpOperationMode", + "radonHealthConcern", + "radonMeasurement", + "rainSensor", + "refresh", + "relativeHumidityMeasurement", + "remoteControlStatus", + "robotCleanerOperatingState", + "safetyValve", + "serviceArea", + "signalStrength", + "smokeDetector", + "soundSensor", + "sounds", + "statelessColorTemperatureStep", + "statelessCurtainPowerButton", + "statelessSwitchLevelStep", + "switch", + "switchLevel", + "tamperAlert", + "temperatureAlarm", + "temperatureLevel", + "temperatureMeasurement", + "temperatureSetpoint", + "thermostatCoolingSetpoint", + "thermostatFanMode", + "thermostatHeatingSetpoint", + "thermostatMode", + "thermostatOperatingState", + "threadBorderRouter", + "threadNetwork", + "threeAxis", + "tone", + "tvocHealthConcern", + "tvocMeasurement", + "ultravioletIndex", + "valve", + "veryFineDustHealthConcern", + "veryFineDustSensor", + "videoCapture2", + "videoStreamSettings", + "voltageMeasurement", + "waterFlowAlarm", + "waterMeter", + "waterSensor", + "webrtc", + "wifiInformation", + "windMode", + "windowShade", + "windowShadeLevel", + "windowShadePreset", + "windowShadeTiltLevel", + "zoneManagement", + "zwMultichannel", +]) + + +def build_headers(pat: str) -> Dict: + return { + "Authorization": f"Bearer {pat}", + "Accept": "application/vnd.smartthings+json;v=1, application/json", + "Content-Type": "application/json", + } + + +def scan_profiles(drivers_dirs: List[Path]) -> Set[Tuple[str, int]]: + """ + Walk every drivers_dir, find all *.yml files under any profiles/ subdirectory, + parse them with PyYAML, and extract (capabilityId, version) pairs. + + Returns a deduplicated set of (id, version) tuples. + """ + found: Set[Tuple[str, int]] = set() + profile_count = 0 + error_count = 0 + + for drivers_dir in drivers_dirs: + for profile_path in drivers_dir.rglob("profiles/*.yml"): + profile_count += 1 + try: + with open(profile_path, encoding="utf-8") as f: + profile = yaml.safe_load(f) + if not profile or not isinstance(profile.get("components"), list): + continue + for component in profile["components"]: + for cap in component.get("capabilities") or []: + cap_id = cap.get("id") + cap_ver = cap.get("version", 1) + if cap_id: + found.add((cap_id, int(cap_ver))) + except Exception as exc: + print(f" WARNING: failed to parse {profile_path}: {exc}", file=sys.stderr) + error_count += 1 + + print( + f"Scanned {profile_count} profile(s) across {len(drivers_dirs)} driver tree(s)" + + (f", {error_count} parse error(s)" if error_count else "") + + "." + ) + return found + + +def output_path(output_dir: Path, cap_id: str, cap_ver: int) -> Path: + return output_dir / f"{cap_id}_{cap_ver}.json" + + +def fetch_definitions( + pairs: List[Tuple[str, int]], + headers: Dict, + max_retries: int = 3, + retry_delay: float = 2.0, +) -> Tuple[Dict[Tuple[str, int], Dict], List[Tuple[str, int, str]]]: + """ + Fetch capability definitions for the given (id, version) pairs. + + Pairs whose ID is in QUERY_ENDPOINT_IDS are fetched via a single bulk POST + to /capabilities/query. All other pairs are fetched individually via + GET /capabilities//, so that an unsupported capability causes + only an isolated failure rather than affecting the entire batch. + + Any capability that is missing from the bulk query response (e.g. due to an + intermittent API hiccup) is retried individually via GET up to max_retries + times before being recorded as a failure. + + Returns a tuple of: + - dict mapping (id, version) -> definition object for successful fetches + - list of (id, version, reason) tuples for failed fetches + """ + results: Dict[Tuple[str, int], Dict] = {} + failures: List[Tuple[str, int, str]] = [] + if not pairs: + return results, failures + + # Split into bulk-query pairs and individual-GET pairs + query_pairs = [(cid, ver) for cid, ver in pairs if cid in QUERY_ENDPOINT_IDS] + get_pairs = [(cid, ver) for cid, ver in pairs if cid not in QUERY_ENDPOINT_IDS] + + # --- Bulk query for known-good capabilities --- + if query_pairs: + query = [{"capabilityId": cid, "version": ver} for cid, ver in query_pairs] + response = requests.post(QUERY_ENDPOINT, headers=headers, json={"query": query}, timeout=30) + + if response.status_code == 200: + for item in response.json().get("items", []): + results[(item["id"], item["version"])] = item + # Any pair missing from the response gets retried individually - the + # API occasionally omits items from the bulk response transiently. + returned = {(item["id"], item["version"]) for item in response.json().get("items", [])} + missing = [(cid, ver) for cid, ver in query_pairs if (cid, ver) not in returned] + if missing: + print( + f" WARNING: {len(missing)} capability/version pair(s) missing from bulk query " + "response — retrying individually..." + ) + for cap_id, cap_ver in missing: + _fetch_individual(cap_id, cap_ver, headers, results, failures, + max_retries, retry_delay) + else: + print( + f" WARNING: bulk query returned HTTP {response.status_code} — " + "marking all query-endpoint capabilities as failed." + ) + for cap_id, cap_ver in query_pairs: + failures.append((cap_id, cap_ver, f"HTTP {response.status_code}")) + + # --- Individual GET for all other capabilities --- + if get_pairs: + print(f" Fetching {len(get_pairs)} capability/version pair(s) via individual GET...") + for cap_id, cap_ver in get_pairs: + _fetch_individual(cap_id, cap_ver, headers, results, failures, + max_retries, retry_delay) + + return results, failures + + +def _fetch_individual( + cap_id: str, + cap_ver: int, + headers: Dict, + results: Dict, + failures: List, + max_retries: int = 3, + retry_delay: float = 2.0, +) -> None: + """ + Fetch a single capability definition via GET /capabilities//, + retrying up to max_retries times on failure with exponential backoff. + Appends to results on success or failures on final failure. + """ + last_status = None + for attempt in range(1, max_retries + 1): + r = requests.get( + f"{API_BASE}/capabilities/{cap_id}/{cap_ver}", + headers=headers, + timeout=30, + ) + if r.status_code == 200: + results[(cap_id, cap_ver)] = r.json() + return + last_status = r.status_code + if attempt < max_retries: + wait = retry_delay * attempt + print( + f" WARNING: {cap_id} v{cap_ver} — HTTP {last_status} " + f"(attempt {attempt}/{max_retries}, retrying in {wait:.0f}s...)" + ) + time.sleep(wait) + + print(f" WARNING: skipping {cap_id} v{cap_ver} — HTTP {last_status} after {max_retries} attempts.") + failures.append((cap_id, cap_ver, f"HTTP {last_status}")) + + +def write_failure_report( + failures: List[Tuple[str, int, str]], + output_file: Path, + truncate_at: int = 10, +) -> None: + """ + Write a markdown-formatted failure report suitable for use as a PR comment. + + The file is only written when there are failures. If failures is empty, + this function does nothing (leaving any previously existing file untouched, + but no file will be created for a clean run). + """ + if not failures: + return + + lines = [ + "## ⚠️ Capability Definition Download Failures", + "", + "The following capability definitions could not be downloaded from the SmartThings API:", + "", + "| Capability ID | Version | Reason |", + "|--------------|---------|--------|", + ] + + shown = failures[:truncate_at] + for cap_id, cap_ver, reason in shown: + lines.append(f"| `{cap_id}` | {cap_ver} | {reason} |") + + if len(failures) > truncate_at: + lines.append("") + lines.append(f"_...and {len(failures) - truncate_at} more._") + + lines += [ + "", + "These capabilities may not be available in the test environment. " + "Tests using these capabilities may fail or fall back to built-in definitions.", + ] + + output_file.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main(argv=None): + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--drivers-dir", + action="append", + dest="drivers_dirs", + metavar="PATH", + required=True, + help="Root of a driver tree to scan (may be specified multiple times).", + ) + parser.add_argument( + "--output-dir", + metavar="PATH", + required=True, + help="Directory to write _.json files.", + ) + parser.add_argument( + "--overwrite", + action="store_true", + default=False, + help=( + "Re-download and overwrite all capability files even if they already " + "exist. Without this flag only missing files are fetched." + ), + ) + parser.add_argument( + "--failed-output-file", + metavar="PATH", + default=None, + help=( + "When set, write a markdown-formatted report of any capabilities that " + "could not be downloaded to this file. The file is only created when " + "there are failures, making it suitable for use as a PR comment via " + "hashFiles() checks in CI workflows." + ), + ) + args = parser.parse_args(argv) + + pat = os.environ.get("CAPABILITY_PAT", "") + if not pat: + print("ERROR: CAPABILITY_PAT environment variable is not set.", file=sys.stderr) + sys.exit(1) + + headers = build_headers(pat) + output_dir = Path(args.output_dir).expanduser().resolve() + output_dir.mkdir(parents=True, exist_ok=True) + + drivers_dirs = [Path(d).expanduser().resolve() for d in args.drivers_dirs] + for d in drivers_dirs: + if not d.is_dir(): + print(f"ERROR: --drivers-dir does not exist: {d}", file=sys.stderr) + sys.exit(1) + + # Step 1: scan profiles + all_pairs = scan_profiles(drivers_dirs) + print(f"Found {len(all_pairs)} unique capability/version pair(s).") + + # Step 2: filter known-bad IDs + excluded = {p for p in all_pairs if p[0] in EXCLUDED_IDS} + for cap_id, cap_ver in sorted(excluded): + print( + f" WARNING: excluding {cap_id} v{cap_ver} " + "(rejected by the query endpoint — likely a duplicate/malformed entry)." + ) + all_pairs -= excluded + + # Step 3: resolve what to fetch + if args.overwrite: + to_fetch = sorted(all_pairs) + already_present = 0 + else: + to_fetch = sorted(p for p in all_pairs if not output_path(output_dir, p[0], p[1]).exists()) + already_present = len(all_pairs) - len(to_fetch) + + if already_present: + print(f"{already_present} capability/version pair(s) already on disk — skipping.") + + if not to_fetch: + print("Nothing to fetch. Output directory is up to date.") + _print_summary(len(drivers_dirs), len(all_pairs) + len(excluded), len(excluded), + already_present, 0, 0, 0, output_dir) + return + + print(f"Fetching {len(to_fetch)} capability definition(s)...") + + # Step 4: fetch + definitions, failures = fetch_definitions(to_fetch, headers) + + # Step 5: write + written = 0 + skipped_api = 0 + for cap_id, cap_ver in to_fetch: + definition = definitions.get((cap_id, cap_ver)) + if definition is None: + skipped_api += 1 + continue + dest = output_path(output_dir, cap_id, cap_ver) + dest.write_text(json.dumps(definition, indent=2), encoding="utf-8") + written += 1 + + _print_summary(len(drivers_dirs), len(all_pairs) + len(excluded), len(excluded), + already_present, len(to_fetch), written, skipped_api, output_dir) + + # Step 6: write failure report if requested + if args.failed_output_file: + failed_output_file = Path(args.failed_output_file).expanduser().resolve() + write_failure_report(failures, failed_output_file) + if failures: + print(f" Failure report written to: {failed_output_file}") + + +def _print_summary( + trees: int, + total_found: int, + excluded: int, + already_present: int, + requested: int, + written: int, + skipped_api: int, + output_dir: Path, +) -> None: + print() + print("=== Summary ===") + print(f" Driver trees scanned : {trees}") + print(f" Unique capabilities : {total_found}") + if excluded: + print(f" Excluded (bad IDs) : {excluded}") + print(f" Already on disk : {already_present}") + print(f" Requested from API : {requested}") + print(f" Written : {written}") + if skipped_api: + print(f" Skipped (API error) : {skipped_api}") + print(f" Output directory : {output_dir}") + + +if __name__ == "__main__": + main() diff --git a/tools/run_driver_tests.py b/tools/run_driver_tests.py index e686a0f4b7..9020bba05b 100755 --- a/tools/run_driver_tests.py +++ b/tools/run_driver_tests.py @@ -38,6 +38,10 @@ def run_tests(verbosity_level, filter, junit, coverage_files, html): total_tests = 0 total_passes = 0 drivers_needing_html = {} + # Propagate ST_CAPABILITY_JSON_DIR so the mock capability channel can load + # capability definitions from pre-fetched JSON files produced by + # tools/fetch_capability_definitions.py. + env = os.environ.copy() for test_file in DRIVER_DIR.glob("*" + os.path.sep + "*" + os.path.sep + "src" + os.path.sep + "test" + os.path.sep + "test_*.lua"): if filter != None and re.search(filter, str(test_file)) is None: continue @@ -46,9 +50,9 @@ def run_tests(verbosity_level, filter, junit, coverage_files, html): print("#" * len(test_line)) print(test_line) if test_file in coverage_files: - a = subprocess.run("lua -lluacov {}".format(test_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + a = subprocess.run("lua -lluacov {}".format(test_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env) else: - a = subprocess.run("lua {}".format(test_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + a = subprocess.run("lua {}".format(test_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env) lines = a.stdout.decode().split("\n") test_count = 0 passes = 0 diff --git a/tools/run_driver_tests_p.py b/tools/run_driver_tests_p.py index 20f22938e9..d19bf46716 100644 --- a/tools/run_driver_tests_p.py +++ b/tools/run_driver_tests_p.py @@ -34,10 +34,14 @@ def per_driver_task(driver_dir): return failure_output def run_test(test_file): + # Propagate ST_CAPABILITY_JSON_DIR so the mock capability channel can load + # capability definitions from pre-fetched JSON files produced by + # tools/fetch_capability_definitions.py. + env = os.environ.copy() if test_file.parent.parent.parent.name in CHANGED_DRIVERS: - a = subprocess.run("lua -lluacov {}".format(test_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + a = subprocess.run("lua -lluacov {}".format(test_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env) else: - a = subprocess.run("lua {}".format(test_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + a = subprocess.run("lua {}".format(test_file), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=env) error = a.stderr.decode() if error and error != "": print(error) From c9c1a245040de5dd841d8eb1c9e803f06299c9ab Mon Sep 17 00:00:00 2001 From: Zach Varberg Date: Fri, 31 Jul 2026 13:18:12 -0500 Subject: [PATCH 081/102] zigbee-bed: Ensure test loaded capabilities load before accessing --- drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua index 50c7512838..2b02b5ec73 100644 --- a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua +++ b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua @@ -7,7 +7,6 @@ local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local custom_capabilities = require "shus-mattress/custom_capabilities" local shus_mattress_profile_def = t_utils.get_profile_definition("shus-smart-mattress.yml") test.add_package_capability("aiMode.yaml") @@ -18,6 +17,8 @@ test.add_package_capability("strongExpMode.yaml") test.add_package_capability("yoga.yaml") test.add_package_capability("mattressHardness.yaml") +local custom_capabilities = require "shus-mattress/custom_capabilities" + local PRIVATE_CLUSTER_ID = 0xFCC2 local MFG_CODE = 0x1235 From a61c2e0aad3c78f765f96c2c3cc3880ff0f0b567 Mon Sep 17 00:00:00 2001 From: chris-baumler Date: Tue, 4 Aug 2026 12:11:29 -0500 Subject: [PATCH 082/102] WWSTCERT-12427 Update smart plug label --- drivers/SmartThings/zwave-switch/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zwave-switch/fingerprints.yml b/drivers/SmartThings/zwave-switch/fingerprints.yml index d0d10f2e5a..2ca0fa3c73 100644 --- a/drivers/SmartThings/zwave-switch/fingerprints.yml +++ b/drivers/SmartThings/zwave-switch/fingerprints.yml @@ -565,7 +565,7 @@ zwaveManufacturer: productId: 0xC001 deviceProfileName: smartplug-binary - id: 0312/FF00/FF07 - deviceLabel: Minoston Outlet + deviceLabel: Smart Plug manufacturerId: 0x0312 productType: 0xFF00 productId: 0xFF07 From a0040ab9cd67dce40190a95b989516405dca7117 Mon Sep 17 00:00:00 2001 From: thinkaName <144081204+thinkaName@users.noreply.github.com> Date: Wed, 5 Aug 2026 03:11:56 +0800 Subject: [PATCH 083/102] firstled_light Change to the correct manufacturer (#3095) --- drivers/SmartThings/zigbee-switch/fingerprints.yml | 4 ++-- .../zigbee-switch/src/firstled-light/fingerprints.lua | 2 +- .../zigbee-switch/src/test/test_firstled_light.lua | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 439bb5da28..08eb875591 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2542,9 +2542,9 @@ zigbeeManufacturer: manufacturer: FIRSTLED model: M2S2BAC deviceProfileName: switch-light-restore-wireless - - id: "ManDai Technology/DC2DC12MiV1" + - id: "FIRSTLED/DC2DC12MiV1" deviceLabel: Easy-Fit Driver - manufacturer: ManDai Technology + manufacturer: FIRSTLED model: DC2DC12MiV1 deviceProfileName: light-color-temp-time-restore zigbeeGeneric: diff --git a/drivers/SmartThings/zigbee-switch/src/firstled-light/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/firstled-light/fingerprints.lua index ba8c2347b3..a406321a2c 100644 --- a/drivers/SmartThings/zigbee-switch/src/firstled-light/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/firstled-light/fingerprints.lua @@ -2,5 +2,5 @@ -- Licensed under the Apache License, Version 2.0 return { - { mfr = "ManDai Technology", model = "DC2DC12MiV1" } + { mfr = "FIRSTLED", model = "DC2DC12MiV1" } } diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_firstled_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_firstled_light.lua index 79b77ab12a..05a04a06ad 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_firstled_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_firstled_light.lua @@ -16,7 +16,7 @@ local mock_device = test.mock_device.build_test_zigbee_device( zigbee_endpoints = { [1] = { id = 1, - manufacturer = "ManDai Technology", + manufacturer = "FIRSTLED", model = "DC2DC12MiV1", server_clusters = { 0x0006, 0x0008, 0x0300 } } From 719780ae565e3a135d97ddcfc210762af37b74d6 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:49:46 -0500 Subject: [PATCH 084/102] Matter Switch: Add logging on create/update of EDGE_CHILD devices (#3166) --- .../matter-switch/src/switch_handlers/attribute_handlers.lua | 4 ++++ .../matter-switch/src/switch_utils/device_configuration.lua | 1 + 2 files changed, 5 insertions(+) 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 753c76992e..ae1f219606 100644 --- a/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/switch_handlers/attribute_handlers.lua @@ -333,6 +333,8 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo device.log.warn("Received an AvailableEndpoints response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with SET feature. Ignoring this response.") return end + + device.log.debug_with({hub_logs=true}, string.format("Handling AvailableEndpoints response for endpoint %d with elements: %s", ib.endpoint_id, st_utils.stringify_table(ib.data.elements or {}))) for i, set_ep_info in pairs(set_topology_eps or {}) do if ib.endpoint_id == set_ep_info.endpoint_id then -- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table @@ -366,6 +368,8 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response) device.log.warn("Received a PartsList response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with TREE feature. Ignoring this response.") return end + + device.log.debug_with({hub_logs=true}, string.format("Handling PartsList response for endpoint %d with elements: %s", ib.endpoint_id, st_utils.stringify_table(ib.data.elements or {}))) for i, tree_ep_info in pairs(tree_topology_eps or {}) do if ib.endpoint_id == tree_ep_info.endpoint_id then -- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table 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 130fd1c1e4..8e5f797877 100644 --- a/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/switch_utils/device_configuration.lua @@ -39,6 +39,7 @@ function ChildConfiguration.create_or_update_child_devices(driver, device, serve local child_profile, optional_component_capabilities = assign_profile_fn(device, ep_id, true) local existing_child_device = device:get_field(fields.IS_PARENT_CHILD_DEVICE) and switch_utils.find_child(device, ep_id) if not existing_child_device then + device.log.info_with({hub_logs=true}, string.format("Creating child device for endpoint %d with profile %s", ep_id, child_profile)) driver:try_create_device({ type = "EDGE_CHILD", label = label_and_name, From bb87a707e2ccd6d3c04aca2beafcf769323983c1 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 6 Aug 2026 16:22:34 -0500 Subject: [PATCH 085/102] WWSTCERT-12180 Schlage Sense Pro (#3025) * WWSTCERT-12180 Schlage Sense Pro * Fix formatting of Schlage Sense Pro fingerprint entry --- drivers/SmartThings/matter-lock/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 7eb674bce9..16117d9119 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -197,6 +197,12 @@ matterManufacturer: vendorId: 0x152C productId: 0x9501 deviceProfileName: lock + #Schlage + - id: "4662/14336" + deviceLabel: Schlage Sense Pro + vendorId: 0x1236 + productId: 0x3800 + deviceProfileName: lock-modular #Lockly - id: "5212/2" deviceLabel: Lockly Smart Lock From 273d954e4d55763665ce8464fd780194471bee55 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Fri, 7 Aug 2026 09:53:03 -0500 Subject: [PATCH 086/102] WWSTCERT-13076 Lockly Smart Lock (#3161) --- drivers/SmartThings/matter-lock/fingerprints.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/SmartThings/matter-lock/fingerprints.yml b/drivers/SmartThings/matter-lock/fingerprints.yml index 16117d9119..b81ae4c48f 100755 --- a/drivers/SmartThings/matter-lock/fingerprints.yml +++ b/drivers/SmartThings/matter-lock/fingerprints.yml @@ -209,6 +209,11 @@ matterManufacturer: vendorId: 0x145C productId: 0x0002 deviceProfileName: lock-modular + - id: "5212/3" + deviceLabel: Lockly Smart Lock + vendorId: 0x145C + productId: 0x0003 + deviceProfileName: lock-modular matterGeneric: - id: "matter/door-lock" deviceLabel: Matter Door Lock From 46305636e93ce3a1f3631faf44186b2abbd6c99b Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Mon, 10 Aug 2026 15:01:03 -0500 Subject: [PATCH 087/102] Updated to report crashing files --- tools/run_driver_tests_p.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/run_driver_tests_p.py b/tools/run_driver_tests_p.py index d19bf46716..c8387dcb1c 100644 --- a/tools/run_driver_tests_p.py +++ b/tools/run_driver_tests_p.py @@ -63,6 +63,12 @@ def run_test(test_file): else: successes += 1 test_cases.append(test_case) + if error and error != "" and len(test_cases) == 0: + failure_output += "\t{} ERROR: test file failed to run\n".format(test_suite_name) + error_case = junit_xml.TestCase("(file error) {}".format(test_suite_name)) + error_case.add_error_info("ERROR", error) + test_cases.append(error_case) + failures += 1 test_suite.test_cases = test_cases return (test_suite, successes, failures, failure_output) From ffe3ea9c1cc9aee679568f08872ce639573bf802 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Tue, 11 Aug 2026 11:34:25 -0500 Subject: [PATCH 088/102] Adding 57 and 58 to the Jenkins driver testing suite --- .github/workflows/jenkins-driver-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/jenkins-driver-tests.yml b/.github/workflows/jenkins-driver-tests.yml index aff7f6f150..6af62c080e 100644 --- a/.github/workflows/jenkins-driver-tests.yml +++ b/.github/workflows/jenkins-driver-tests.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: version: - [ 59, 60, 61, dev] + [57, 58, 59, 60, 61, dev] runs-on: ubuntu-latest steps: From 248ccf02a591aaea9972bd848744488e27aad210 Mon Sep 17 00:00:00 2001 From: Alec Lorimer Date: Wed, 5 Aug 2026 12:24:48 -0500 Subject: [PATCH 089/102] Updated min_api_version for tests on v14(57) and v15(58) - Clamped matter-air_purifier_api9.lua to version 9 - Filtering of test_matter_camera and test_matter_irrigation_system files for pre 59 --- .../src/test/test_cook_top.lua | 8 +- .../src/test/test_dishwasher.lua | 16 +- .../src/test/test_extractor_hood.lua | 22 +- .../src/test/test_laundry_dryer.lua | 18 +- .../src/test/test_laundry_washer.lua | 4 +- .../src/test/test_matter_appliance_rpc_5.lua | 24 +- .../src/test/test_microwave_oven.lua | 10 +- .../matter-appliance/src/test/test_oven.lua | 20 +- .../src/test/test_refrigerator.lua | 10 +- .../src/test/test_matter_button.lua | 20 +- .../test/test_matter_button_parent_child.lua | 16 +- .../src/test/test_matter_multi_button.lua | 38 +-- .../src/test/test_battery_storage.lua | 10 +- .../matter-energy/src/test/test_evse.lua | 24 +- .../src/test/test_evse_energy_meas.lua | 4 +- .../src/test/test_solar_power.lua | 6 +- .../test_thread_border_router_network.lua | 14 +- .../src/test/test_aqara_matter_lock.lua | 16 +- .../src/test/test_bridged_matter_lock.lua | 4 +- .../matter-lock/src/test/test_matter_lock.lua | 24 +- .../src/test/test_matter_lock_battery.lua | 6 +- .../test/test_matter_lock_batteryLevel.lua | 2 +- .../src/test/test_matter_lock_codes.lua | 38 +-- .../src/test/test_matter_lock_cota.lua | 26 +- .../src/test/test_matter_lock_migration.lua | 286 +++++++++--------- .../src/test/test_matter_lock_modular.lua | 26 +- .../src/test/test_matter_lock_unlatch.lua | 18 +- .../src/test/test_new_matter_lock.lua | 104 +++---- .../src/test/test_new_matter_lock_aliro.lua | 86 ++++-- .../src/test/test_new_matter_lock_battery.lua | 28 +- .../src/test/test_matter_media_speaker.lua | 10 +- .../test/test_matter_media_video_player.lua | 18 +- .../matter-rvc/src/test/test_matter_rvc.lua | 40 +-- .../test/test_matter_air_quality_sensor.lua | 22 +- ...test_matter_air_quality_sensor_modular.lua | 25 +- .../src/test/test_matter_aqara_fp400.lua | 8 +- .../test/test_matter_bosch_button_contact.lua | 14 +- .../src/test/test_matter_flow_sensor.lua | 6 +- .../test/test_matter_freeze_leak_sensor.lua | 12 +- .../src/test/test_matter_pressure_sensor.lua | 6 +- .../src/test/test_matter_rain_sensor.lua | 4 +- .../src/test/test_matter_sensor.lua | 18 +- .../src/test/test_matter_sensor_battery.lua | 6 +- .../test/test_matter_sensor_featuremap.lua | 6 +- .../src/test/test_matter_sensor_rpc.lua | 2 +- .../src/test/test_matter_smoke_co_alarm.lua | 22 +- .../test_matter_smoke_co_alarm_battery.lua | 6 +- .../src/test/test_matter_soil_sensor.lua | 35 ++- .../test/test_aqara_climate_sensor_w100.lua | 28 +- .../src/test/test_aqara_cube.lua | 4 +- .../src/test/test_aqara_light_switch_h2.lua | 4 +- .../src/test/test_electrical_sensor_set.lua | 18 +- .../src/test/test_electrical_sensor_tree.lua | 8 +- .../src/test/test_eve_energy.lua | 30 +- .../src/test/test_hager_waasys.lua | 55 +++- .../src/test/test_ikea_scroll.lua | 31 +- .../test/test_light_illuminance_motion.lua | 13 +- .../src/test/test_matter_bridge.lua | 2 +- .../src/test/test_matter_button.lua | 36 +-- .../src/test/test_matter_camera.lua | 107 ++++--- .../test/test_matter_irrigation_system.lua | 8 + .../src/test/test_matter_light_fan.lua | 18 +- .../src/test/test_matter_multi_button.lua | 48 +-- .../test/test_matter_multi_button_motion.lua | 40 +-- .../test_matter_multi_button_switch_mcd.lua | 18 +- ...est_matter_on_off_device_configuration.lua | 14 +- .../test/test_matter_on_off_parent_child.lua | 29 +- .../test_matter_sensor_offset_preferences.lua | 4 +- .../src/test/test_matter_switch.lua | 54 ++-- .../src/test/test_matter_water_valve.lua | 17 +- .../src/test/test_multi_switch_mcd.lua | 6 +- .../src/test/test_stateless_step.lua | 4 +- .../test/test_third_reality_subdrivers.lua | 25 +- .../src/test/test_matter_air_purifier.lua | 26 +- .../test/test_matter_air_purifier_api9.lua | 42 ++- .../test/test_matter_air_purifier_modular.lua | 4 +- .../src/test/test_matter_fan.lua | 4 +- .../src/test/test_matter_heat_pump.lua | 26 +- .../src/test/test_matter_room_ac.lua | 16 +- .../src/test/test_matter_room_ac_modular.lua | 4 +- .../src/test/test_matter_thermo_battery.lua | 6 +- .../test/test_matter_thermo_featuremap.lua | 8 +- ...st_matter_thermo_multiple_device_types.lua | 6 +- .../test_matter_thermo_setpoint_limits.lua | 20 +- ...test_matter_thermo_setpoint_limits_rpc.lua | 6 +- .../src/test/test_matter_thermostat.lua | 48 +-- ...est_matter_thermostat_composed_bridged.lua | 42 +-- .../test/test_matter_thermostat_modular.lua | 22 +- .../src/test/test_matter_thermostat_rpc5.lua | 2 +- .../src/test/test_matter_water_heater.lua | 14 +- .../src/test/test_matter_closure.lua | 127 ++++++-- .../src/test/test_matter_window_covering.lua | 5 +- .../src/test/test_virtual_switch.lua | 14 +- .../test_MultiIR_air_quality_detector.lua | 44 +-- .../src/test/test_shus_mattress.lua | 112 +++---- .../src/test/test_SLED_button.lua | 6 +- .../src/test/test_aduro_button.lua | 6 +- .../src/test/test_aqara_button.lua | 22 +- .../src/test/test_centralite_button.lua | 10 +- .../src/test/test_dimming_remote.lua | 12 +- .../src/test/test_ewelink_button.lua | 10 +- .../src/test/test_ezviz_button.lua | 12 +- .../src/test/test_frient_button.lua | 18 +- .../src/test/test_heiman_button.lua | 16 +- .../src/test/test_ikea_on_off.lua | 10 +- .../src/test/test_ikea_open_close.lua | 10 +- .../src/test/test_ikea_remote_control.lua | 10 +- .../src/test/test_iris_button.lua | 18 +- .../test/test_linxura_aura_smart_button.lua | 8 +- ...est_linxura_smart_controller_4x_button.lua | 8 +- .../src/test/test_push_only_button.lua | 12 +- .../src/test/test_robb_4x_button.lua | 14 +- .../src/test/test_robb_8x_button.lua | 14 +- .../src/test/test_samjin_button.lua | 4 +- .../src/test/test_shinasystem_button.lua | 10 +- .../src/test/test_somfy_situo_1_button.lua | 10 +- .../src/test/test_somfy_situo_4_button.lua | 8 +- .../src/test/test_sonoff_button.lua | 14 +- .../src/test/test_thirdreality_button.lua | 8 +- .../src/test/test_vimar_button.lua | 12 +- .../src/test/test_wallhero_button.lua | 4 +- .../src/test/test_zigbee_button.lua | 24 +- .../src/test/test_zigbee_ecosmart_button.lua | 14 +- .../src/test/test_zunzunbee_8_button.lua | 8 +- ...test_climax_technology_carbon_monoxide.lua | 2 +- ...st_frient_co_smoke_temperature_battery.lua | 15 +- .../src/test/test_zigbee_carbon_monoxide.lua | 12 +- .../src/test/test_aqara_contact_sensor.lua | 18 +- .../src/test/test_aurora_contact_sensor.lua | 4 +- .../src/test/test_centralite_multi_sensor.lua | 16 +- .../test/test_contact_temperature_sensor.lua | 8 +- .../src/test/test_ecolink_contact.lua | 8 +- .../src/test/test_ewelink_heiman_sensor.lua | 4 +- .../src/test/test_frient_contact_sensor.lua | 18 +- .../test/test_frient_contact_sensor_2_pro.lua | 22 +- .../test/test_frient_contact_sensor_pro.lua | 22 +- .../src/test/test_frient_vibration_sensor.lua | 20 +- .../src/test/test_orvibo_contact_sensor.lua | 4 +- .../src/test/test_samjin_multi_sensor.lua | 8 +- .../src/test/test_sengled_contact_sensor.lua | 4 +- .../src/test/test_smartsense_multi.lua | 52 ++-- .../test/test_smartthings_multi_sensor.lua | 26 +- .../src/test/test_third_reality_contact.lua | 4 +- .../test/test_thirdreality_multi_sensor.lua | 6 +- .../src/test/test_zigbee_contact.lua | 18 +- .../src/test/test_zigbee_contact_battery.lua | 6 +- .../src/test/test_zigbee_contact_tyco.lua | 6 +- .../zigbee-fan/src/test/test_fan_light.lua | 42 +-- .../src/test/test_aqara_sensor.lua | 20 +- .../src/test/test_centralite_sensor.lua | 6 +- .../src/test/test_ewelink_sensor.lua | 4 +- .../test/test_frient_air_quality_sensor.lua | 18 +- .../src/test/test_frient_sensor.lua | 14 +- .../src/test/test_heiman_sensor.lua | 4 +- .../src/test/test_humidity_battery_sensor.lua | 8 +- .../src/test/test_humidity_plaid_systems.lua | 18 +- .../src/test/test_humidity_temperature.lua | 10 +- .../test_humidity_temperature_battery.lua | 10 +- .../test/test_humidity_temperature_sensor.lua | 8 +- .../src/test/test_illuminance_sensor.lua | 6 +- .../test/test_illuminance_sensor_aqara.lua | 12 +- .../src/test/test_bad_battery_reporter.lua | 4 +- .../zigbee-lock/src/test/test_c2o_lock.lua | 16 +- ...est_generic_fingerprint_profile_update.lua | 2 +- .../src/test/test_zigbee_lock_legacy.lua | 56 ++-- .../src/test/test_zigbee_lock_v10.lua | 48 +-- .../test_zigbee_yale_fingerprint-lock.lua | 2 +- .../src/test/test_zigbee_yale_legacy.lua | 22 +- .../test_all_capabilities_zigbee_motion.lua | 24 +- .../src/test/test_aqara_high_precision.lua | 18 +- .../test/test_aqara_motion_illuminance.lua | 12 +- .../src/test/test_aurora_motion.lua | 12 +- .../src/test/test_battery_voltage_motion.lua | 2 +- .../src/test/test_centralite_motion.lua | 4 +- .../src/test/test_compacta_motion.lua | 4 +- .../src/test/test_frient_motion_sensor.lua | 12 +- .../test/test_frient_motion_sensor2_pet.lua | 14 +- .../test/test_frient_motion_sensor_pro.lua | 22 +- .../src/test/test_gator_motion.lua | 18 +- .../src/test/test_ikea_motion.lua | 14 +- .../src/test/test_samjin_sensor.lua | 4 +- .../src/test/test_sengled_motion.lua | 4 +- .../test/test_smartsense_motion_sensor.lua | 16 +- .../src/test/test_smartthings_motion.lua | 2 +- .../src/test/test_thirdreality_sensor.lua | 10 +- .../src/test/test_zigbee_motion_iris.lua | 4 +- .../src/test/test_zigbee_motion_nyce.lua | 6 +- .../src/test/test_zigbee_motion_orvibo.lua | 12 +- .../test/test_zigbee_plugin_motion_sensor.lua | 8 +- ...ower_energy_battery_consumption_report.lua | 5 +- ...frient_power_energy_consumption_report.lua | 5 +- ...st_frient_power_energy_current_voltage.lua | 5 +- .../src/test/test_zigbee_power_meter.lua | 14 +- .../src/test/test_zigbee_power_meter_1p.lua | 20 +- .../test/test_zigbee_power_meter_1p_ZM.lua | 5 +- .../src/test/test_zigbee_power_meter_2p.lua | 16 +- .../test/test_zigbee_power_meter_2p_ZM.lua | 5 +- .../src/test/test_zigbee_power_meter_3p.lua | 22 +- .../test/test_zigbee_power_meter_3p_ZM.lua | 5 +- ...e_power_meter_consumption_report_sihas.lua | 12 +- .../src/test/test_zigbee_power_meter_ezex.lua | 8 +- .../test/test_zigbee_power_meter_frient.lua | 2 +- .../test/test_aqara_presence_sensor_fp1.lua | 20 +- .../src/test/test_st_arrival_sensor_v1.lua | 16 +- .../src/test/test_zigbee_presence_sensor.lua | 26 +- .../test_frient_zigbee_range_extender.lua | 18 +- .../src/test/test_zigbee_extend.lua | 2 +- .../src/test/test_zigbee_sensor.lua | 66 ++-- .../src/test/test_frient_siren.lua | 46 +-- .../src/test/test_frient_siren_tamper.lua | 42 +-- .../zigbee-siren/src/test/test_ozom_siren.lua | 6 +- .../src/test/test_aqara_gas_detector.lua | 34 +-- .../src/test/test_aqara_smoke_detector.lua | 22 +- .../src/test/test_frient_heat_detector.lua | 32 +- .../src/test/test_frient_smoke_detector.lua | 36 +-- .../src/test/test_zigbee_smoke_detector.lua | 14 +- .../src/test/test_zigbee_sound_sensor.lua | 16 +- .../test/test_all_capability_zigbee_bulb.lua | 28 +- .../src/test/test_aqara_led_bulb.lua | 8 +- .../src/test/test_aqara_light.lua | 16 +- .../src/test/test_aqara_smart_plug.lua | 28 +- .../src/test/test_aqara_smart_plug_t1.lua | 30 +- .../src/test/test_aqara_switch_module.lua | 18 +- .../test_aqara_switch_module_no_power.lua | 16 +- .../src/test/test_aqara_switch_no_power.lua | 32 +- .../src/test/test_aqara_switch_power.lua | 34 +-- .../src/test/test_aqara_wall_switch.lua | 22 +- .../src/test/test_aurora_relay.lua | 8 +- .../src/test/test_bad_data_type.lua | 2 +- .../src/test/test_bad_device_kind.lua | 4 +- .../zigbee-switch/src/test/test_cree_bulb.lua | 8 +- .../test/test_duragreen_color_temp_bulb.lua | 8 +- .../test/test_enbrighten_metering_dimmer.lua | 14 +- .../src/test/test_frient_IO_module.lua | 10 +- .../src/test/test_frient_switch.lua | 20 +- .../src/test/test_ge_link_bulb.lua | 14 +- .../src/test/test_hanssem_switch.lua | 50 +-- .../src/test/test_inovelli_vzm30_sn.lua | 28 +- .../src/test/test_inovelli_vzm30_sn_child.lua | 12 +- .../test_inovelli_vzm30_sn_preferences.lua | 16 +- .../src/test/test_inovelli_vzm31_sn.lua | 22 +- .../src/test/test_inovelli_vzm31_sn_child.lua | 12 +- .../test_inovelli_vzm31_sn_preferences.lua | 16 +- .../src/test/test_inovelli_vzm32_sn.lua | 30 +- .../src/test/test_inovelli_vzm32_sn_child.lua | 12 +- .../test_inovelli_vzm32_sn_preferences.lua | 12 +- .../src/test/test_jasco_switch.lua | 10 +- .../src/test/test_laisiao_bath_heather.lua | 64 ++-- .../src/test/test_ledvance_metering_plug.lua | 8 +- .../src/test/test_multi_switch.lua | 10 +- .../src/test/test_multi_switch_no_master.lua | 28 +- .../src/test/test_multi_switch_power.lua | 32 +- .../src/test/test_on_off_zigbee_bulb.lua | 10 +- .../src/test/test_osram_iqbr30_light.lua | 8 +- .../src/test/test_osram_light.lua | 6 +- .../zigbee-switch/src/test/test_rgb_bulb.lua | 10 +- .../zigbee-switch/src/test/test_rgbw_bulb.lua | 14 +- .../test/test_robb_smarrt_2-wire_dimmer.lua | 10 +- .../src/test/test_robb_smarrt_knob_dimmer.lua | 8 +- .../src/test/test_sengled_color_temp_bulb.lua | 6 +- ...sengled_dimmer_bulb_with_motion_sensor.lua | 16 +- .../src/test/test_sinope_dimmer.lua | 18 +- .../src/test/test_sinope_switch.lua | 6 +- .../src/test/test_switch_power.lua | 8 +- .../src/test/test_tuya_multi.lua | 2 +- .../src/test/test_tuya_multi_switch.lua | 10 +- .../src/test/test_wallhero_switch.lua | 48 +-- .../src/test/test_white_color_temp_bulb.lua | 6 +- .../src/test/test_yanmi_switch.lua | 24 +- .../src/test/test_zigbee_ezex_switch.lua | 10 +- ...metering_plug_power_consumption_report.lua | 6 +- .../test_zigbee_metering_plug_rexense.lua | 4 +- .../src/test/test_zll_color_temp_bulb.lua | 12 +- .../src/test/test_zll_dimmer.lua | 10 +- .../src/test/test_zll_dimmer_bulb.lua | 14 +- .../src/test/test_zll_rgb_bulb.lua | 32 +- .../src/test/test_zll_rgbw_bulb.lua | 18 +- .../src/test/test_aqara_thermostat.lua | 24 +- .../src/test/test_centralite_thermostat.lua | 10 +- .../src/test/test_danfoss_thermostat.lua | 12 +- .../src/test/test_fidure_thermostat.lua | 4 +- .../src/test/test_leviton_rc.lua | 28 +- .../src/test/test_popp_thermostat.lua | 30 +- .../src/test/test_resideo_dt300st_m000.lua | 100 +++--- .../test/test_sinope_th1300_thermostat.lua | 8 +- .../test/test_sinope_th1400_thermostat.lua | 8 +- .../src/test/test_sinope_thermostat.lua | 10 +- .../test_stelpro_ki_zigbee_thermostat.lua | 36 +-- .../src/test/test_stelpro_thermostat.lua | 38 +-- .../src/test/test_vimar_thermostat.lua | 38 +-- .../src/test/test_zenwithin_thermostat.lua | 40 +-- .../src/test/test_zigbee_thermostat.lua | 52 ++-- .../test_zigbee_thermostat_wallhero_3in1.lua | 86 +++--- .../zigbee-valve/src/test/test_ezex_valve.lua | 26 +- .../src/test/test_sinope_valve.lua | 12 +- .../src/test/test_zigbee_valve.lua | 24 +- .../zigbee-vent/src/test/test_zigbee_vent.lua | 22 +- .../src/test/test_aqara_water_leak_sensor.lua | 8 +- .../test_centralite_water_leak_sensor.lua | 18 +- .../test/test_frient_water_leak_sensor.lua | 18 +- .../src/test/test_leaksmart_water.lua | 14 +- .../test/test_samjin_water_leak_sensor.lua | 14 +- .../test/test_sengled_water_leak_sensor.lua | 4 +- .../src/test/test_sinope_zigbee_water.lua | 18 +- .../test_smartthings_water_leak_sensor.lua | 12 +- .../test_thirdreality_water_leak_sensor.lua | 12 +- .../src/test/test_zigbee_water.lua | 18 +- .../src/test/test_zigbee_water_freeze.lua | 10 +- .../test/test_thirdreality_watering_kit.lua | 32 +- .../test_zigbee_window_shade_battery_ikea.lua | 20 +- ...est_zigbee_window_shade_only_HOPOsmart.lua | 18 +- .../src/test/test_zigbee_window_treatment.lua | 32 +- ..._zigbee_window_treatment_VWSDSTUST120H.lua | 34 +-- .../test_zigbee_window_treatment_aqara.lua | 40 +-- ...ndow_treatment_aqara_curtain_driver_e1.lua | 30 +- ...ow_treatment_aqara_roller_shade_rotate.lua | 32 +- .../test_zigbee_window_treatment_axis.lua | 34 +-- .../test_zigbee_window_treatment_rooms.lua | 26 +- ...ee_window_treatment_screen_innovations.lua | 28 +- .../test_zigbee_window_treatment_sombra.lua | 28 +- .../test_zigbee_window_treatment_somfy.lua | 38 +-- .../src/test/test_aeon_multiwhite_bulb.lua | 36 +-- .../src/test/test_aeotec_led_bulb_6.lua | 6 +- .../src/test/test_fibaro_rgbw_controller.lua | 24 +- .../zwave-bulb/src/test/test_zwave_bulb.lua | 20 +- .../test/test_zwave_aeotec_nanomote_one.lua | 2 +- .../src/test/test_zwave_button.lua | 12 +- .../src/test/test_zwave_fibaro_button.lua | 2 +- .../src/test/test_zwave_multi_button.lua | 44 +-- .../src/test/test_aeon_meter.lua | 8 +- .../src/test/test_aeotec_gen5_meter.lua | 8 +- .../src/test/test_qubino_3_phase_meter.lua | 8 +- .../src/test/test_qubino_smart_meter.lua | 8 +- .../src/test/test_zwave_electric_meter.lua | 6 +- .../src/test/test_zwave_fan_3_speed.lua | 10 +- .../src/test/test_zwave_fan_4_speed.lua | 10 +- .../test_ecolink_garage_door_operator.lua | 22 +- .../src/test/test_mimolite_garage_door.lua | 24 +- .../test/test_zwave_garage_door_opener.lua | 14 +- .../zwave-lock/src/test/test_keywe_lock.lua | 5 +- .../src/test/test_keywe_lock_legacy.lua | 4 +- .../zwave-lock/src/test/test_lock_battery.lua | 12 +- .../test/test_lock_credentials_commands.lua | 65 +++- .../src/test/test_lock_pre_configured.lua | 50 ++- .../src/test/test_lock_users_commands.lua | 65 +++- .../src/test/test_samsung_lock_legacy.lua | 10 +- .../src/test/test_schlage_lock_legacy.lua | 16 +- .../src/test/test_zwave_lock_legacy.lua | 46 +-- .../src/test/test_zwave_responses.lua | 55 +++- .../src/test/test_zwave_mouse_trap.lua | 24 +- .../src/test/test_aeon_multisensor.lua | 4 +- .../src/test/test_aeotec_multisensor_6.lua | 22 +- .../src/test/test_aeotec_multisensor_7.lua | 12 +- .../src/test/test_aeotec_multisensor_gen5.lua | 2 +- .../src/test/test_aeotec_water_sensor.lua | 24 +- .../src/test/test_aeotec_water_sensor_7.lua | 12 +- .../src/test/test_enerwave_motion_sensor.lua | 6 +- .../src/test/test_everpsring_sp817.lua | 4 +- .../src/test/test_everspring_PIR_sensor.lua | 12 +- .../src/test/test_everspring_ST814.lua | 2 +- .../test_everspring_illuminance_sensor.lua | 2 +- .../test_everspring_motion_light_sensor.lua | 2 +- .../test_ezmultipli_multipurpose_sensor.lua | 10 +- .../test/test_fibaro_door_window_sensor.lua | 14 +- .../test/test_fibaro_door_window_sensor_1.lua | 22 +- .../test/test_fibaro_door_window_sensor_2.lua | 20 +- ...ro_door_window_sensor_with_temperature.lua | 20 +- .../src/test/test_fibaro_flood_sensor.lua | 24 +- .../src/test/test_fibaro_flood_sensor_zw5.lua | 2 +- .../src/test/test_fibaro_motion_sensor.lua | 26 +- .../test/test_fibaro_motion_sensor_zw5.lua | 4 +- .../src/test/test_firmware_version.lua | 6 +- .../src/test/test_generic_sensor.lua | 118 ++++---- .../test_glentronics_water_leak_sensor.lua | 14 +- .../src/test/test_homeseer_multi_sensor.lua | 10 +- .../src/test/test_no_wakeup_poll.lua | 2 +- .../src/test/test_sensative_strip.lua | 6 +- .../test_smartthings_water_leak_sensor.lua | 24 +- .../src/test/test_v1_contact_event.lua | 6 +- .../src/test/test_vision_motion_detector.lua | 12 +- .../src/test/test_zooz_4_in_1_sensor.lua | 20 +- .../test/test_zwave_motion_light_sensor.lua | 22 +- .../test_zwave_motion_temp_light_sensor.lua | 18 +- .../src/test/test_zwave_sensor.lua | 54 ++-- .../src/test/test_zwave_water_sensor.lua | 24 +- .../zwave-siren/src/test/test_aeon_siren.lua | 20 +- .../src/test/test_aeotec_doorbell_siren.lua | 160 +++++----- .../src/test/test_ecolink_wireless_siren.lua | 30 +- .../src/test/test_fortrezz_siren.lua | 8 +- .../src/test/test_philio_sound_siren.lua | 36 +-- .../src/test/test_utilitech_siren.lua | 4 +- .../zwave-siren/src/test/test_yale_siren.lua | 18 +- .../src/test/test_zipato_siren.lua | 10 +- .../zwave-siren/src/test/test_zooz_zse50.lua | 48 +-- .../test/test_zwave_multifunctional-siren.lua | 10 +- .../test/test_zwave_notification_siren.lua | 8 +- .../zwave-siren/src/test/test_zwave_siren.lua | 28 +- .../src/test/test_zwave_sound_sensor.lua | 6 +- .../src/test/test_fibaro_co_sensor_zw5.lua | 28 +- .../src/test/test_fibaro_smoke_sensor.lua | 6 +- .../src/test/test_zwave_alarm_v1.lua | 14 +- .../src/test/test_zwave_co_detector.lua | 14 +- .../src/test/test_zwave_smoke_detector.lua | 30 +- .../src/test/test_aeon_smart_strip.lua | 32 +- .../src/test/test_aeotec_dimmer_switch.lua | 22 +- ..._aeotec_dual_nano_switch_configuration.lua | 2 +- .../test/test_aeotec_heavy_duty_switch.lua | 36 +-- ...t_aeotec_metering_switch_configuration.lua | 2 +- .../src/test/test_aeotec_nano_dimmer.lua | 22 +- .../test_aeotec_nano_dimmer_preferences.lua | 2 +- .../src/test/test_aeotec_smart_switch.lua | 4 +- .../test/test_aeotec_smart_switch_7_eu.lua | 12 +- .../test/test_aeotec_smart_switch_7_us.lua | 12 +- .../test/test_aeotec_smart_switch_gen5.lua | 2 +- .../src/test/test_dawon_smart_plug.lua | 4 +- .../src/test/test_dawon_wall_smart_switch.lua | 20 +- .../src/test/test_eaton_5_scene_keypad.lua | 26 +- .../src/test/test_eaton_accessory_dimmer.lua | 16 +- .../src/test/test_eaton_anyplace_switch.lua | 12 +- .../src/test/test_eaton_rf_dimmer.lua | 2 +- .../src/test/test_ecolink_switch.lua | 18 +- .../src/test/test_fibaro_double_switch.lua | 20 +- .../src/test/test_fibaro_single_switch.lua | 42 +-- .../src/test/test_fibaro_wall_plug_eu.lua | 2 +- ...test_fibaro_wall_plug_uk_configuration.lua | 2 +- .../src/test/test_fibaro_wall_plug_us.lua | 16 +- .../test_fibaro_walli_dimmer_preferences.lua | 14 +- .../test/test_fibaro_walli_double_switch.lua | 30 +- ...fibaro_walli_double_switch_preferences.lua | 12 +- .../src/test/test_generic_zwave_device1.lua | 16 +- ...go_control_plug_in_switch_configuraton.lua | 2 +- .../src/test/test_honeywell_dimmer.lua | 2 +- .../test_inovelli_2_channel_smart_plug.lua | 36 +-- .../src/test/test_inovelli_button.lua | 8 +- .../src/test/test_inovelli_dimmer.lua | 14 +- .../src/test/test_inovelli_dimmer_led.lua | 4 +- .../test_inovelli_dimmer_power_energy.lua | 18 +- .../test/test_inovelli_dimmer_preferences.lua | 10 +- .../src/test/test_inovelli_dimmer_scenes.lua | 22 +- .../src/test/test_inovelli_vzw32_sn.lua | 14 +- .../src/test/test_inovelli_vzw32_sn_child.lua | 12 +- .../test_inovelli_vzw32_sn_preferences.lua | 10 +- .../src/test/test_multi_metering_switch.lua | 36 +-- .../src/test/test_multichannel_device.lua | 118 ++++---- .../test_popp_outdoor_plug_configuration.lua | 2 +- .../src/test/test_qubino_din_dimmer.lua | 24 +- .../test_qubino_din_dimmer_preferences.lua | 14 +- .../test_qubino_flush_1_relay_preferences.lua | 10 +- ...test_qubino_flush_1d_relay_preferences.lua | 6 +- .../src/test/test_qubino_flush_2_relay.lua | 34 +-- .../test_qubino_flush_2_relay_preferences.lua | 10 +- .../src/test/test_qubino_flush_dimmer.lua | 24 +- ..._qubino_flush_dimmer_0_10V_preferences.lua | 14 +- .../test_qubino_flush_dimmer_preferences.lua | 18 +- .../test_qubino_mini_dimmer_preferences.lua | 16 +- ...t_qubino_temperature_sensor_with_power.lua | 18 +- ...ubino_temperature_sensor_without_power.lua | 12 +- .../test_shelly_multi_metering_switch.lua | 34 +-- .../zwave-switch/src/test/test_wyfy_touch.lua | 22 +- .../test/test_wyfy_touch_configuration.lua | 2 +- .../src/test/test_zooz_double_plug.lua | 30 +- .../src/test/test_zooz_power_strip.lua | 66 ++-- .../test/test_zooz_zen_30_dimmer_relay.lua | 80 ++--- ...t_zooz_zen_30_dimmer_relay_preferences.lua | 2 +- .../test/test_zwave_dimmer_power_energy.lua | 18 +- .../src/test/test_zwave_dual_switch.lua | 36 +-- .../test/test_zwave_dual_switch_migration.lua | 4 +- .../src/test/test_zwave_switch.lua | 20 +- .../src/test/test_zwave_switch_battery.lua | 6 +- .../test/test_zwave_switch_electric_meter.lua | 8 +- .../test/test_zwave_switch_energy_meter.lua | 10 +- .../src/test/test_zwave_switch_level.lua | 4 +- .../test/test_zwave_switch_power_meter.lua | 8 +- .../test/test_aeotec_radiator_thermostat.lua | 16 +- .../src/test/test_ct100_thermostat.lua | 20 +- .../src/test/test_fibaro_heat_controller.lua | 22 +- .../test/test_popp_radiator_thermostat.lua | 12 +- .../src/test/test_qubino_flush_thermostat.lua | 30 +- .../src/test/test_stelpro_ki_thermostat.lua | 20 +- .../test/test_thermostat_heating_battery.lua | 28 +- .../src/test/test_zwave_thermostat.lua | 44 +-- .../src/test/test_inverse.valve.lua | 12 +- .../zwave-valve/src/test/test_zwave_valve.lua | 16 +- .../test_zwave_virtual_momentary_switch.lua | 14 +- .../src/test/test_fibaro_roller_shutter.lua | 60 ++-- .../src/test/test_qubino_flush_shutter.lua | 50 +-- .../test/test_zwave_aeotec_nano_shutter.lua | 24 +- .../src/test/test_tuya_curtain.lua | 77 ++++- .../tuya-zigbee/src/test/test_tuya_switch.lua | 35 ++- .../src/test/test_tuya_thermostat.lua | 24 +- 490 files changed, 5111 insertions(+), 4585 deletions(-) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua index 0321d956ce..4c489ee38d 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_cook_top.lua @@ -91,7 +91,7 @@ test.register_coroutine_test( assert(component_to_endpoint_map["cookSurfaceTwo"] == COOK_SURFACE_TWO_ENDPOINT, "Cook Surface Two Endpoint must be 3") end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -157,7 +157,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -192,7 +192,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua index 6819815506..93b6bbc3f3 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_dishwasher.lua @@ -103,7 +103,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -171,7 +171,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -239,7 +239,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -307,7 +307,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -386,7 +386,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -450,7 +450,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -490,7 +490,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -549,7 +549,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua b/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua index d176352f80..f326ba50b8 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_extractor_hood.lua @@ -176,7 +176,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -250,7 +250,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -339,7 +339,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) test.register_message_test( @@ -452,7 +452,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -490,7 +490,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -531,7 +531,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -592,7 +592,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -653,7 +653,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -666,7 +666,7 @@ test.register_coroutine_test( end, { test_init = test_init_onoff, - min_api_version = 17 + min_api_version = 15 } ) @@ -692,7 +692,7 @@ test.register_coroutine_test( end, { test_init = test_init_onoff, - min_api_version = 17 + min_api_version = 15 } ) @@ -716,7 +716,7 @@ test.register_coroutine_test( end, { test_init = test_init_onoff, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua index ba1440af85..d942817d57 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_dryer.lua @@ -102,7 +102,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -170,7 +170,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -238,7 +238,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -306,7 +306,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -385,7 +385,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -449,7 +449,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -489,7 +489,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -548,7 +548,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -591,7 +591,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua b/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua index 6aba845a27..288d76778e 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_laundry_washer.lua @@ -135,7 +135,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -178,7 +178,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_matter_appliance_rpc_5.lua b/drivers/SmartThings/matter-appliance/src/test/test_matter_appliance_rpc_5.lua index 7e59d7925f..cc270cdddb 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_matter_appliance_rpc_5.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_matter_appliance_rpc_5.lua @@ -458,7 +458,7 @@ test.register_coroutine_test( end, { test_init = test_init_dishwasher, - min_api_version = 17 + min_api_version = 15 } ) @@ -500,7 +500,7 @@ test.register_coroutine_test( end, { test_init = test_init_dishwasher, - min_api_version = 17 + min_api_version = 15 } ) @@ -542,7 +542,7 @@ test.register_coroutine_test( end, { test_init = test_init_washer, - min_api_version = 17 + min_api_version = 15 } ) @@ -584,7 +584,7 @@ test.register_coroutine_test( end, { test_init = test_init_washer, - min_api_version = 17 + min_api_version = 15 } ) @@ -626,7 +626,7 @@ test.register_coroutine_test( end, { test_init = test_init_dryer, - min_api_version = 17 + min_api_version = 15 } ) @@ -668,7 +668,7 @@ test.register_coroutine_test( end, { test_init = test_init_dryer, - min_api_version = 17 + min_api_version = 15 } ) @@ -710,7 +710,7 @@ test.register_coroutine_test( end, { test_init = test_init_oven, - min_api_version = 17 + min_api_version = 15 } ) @@ -752,7 +752,7 @@ test.register_coroutine_test( end, { test_init = test_init_oven, - min_api_version = 17 + min_api_version = 15 } ) @@ -794,7 +794,7 @@ test.register_coroutine_test( end, { test_init = test_init_refrigerator, - min_api_version = 17 + min_api_version = 15 } ) @@ -836,7 +836,7 @@ test.register_coroutine_test( end, { test_init = test_init_refrigerator, - min_api_version = 17 + min_api_version = 15 } ) @@ -878,7 +878,7 @@ test.register_coroutine_test( end, { test_init = test_init_refrigerator, - min_api_version = 17 + min_api_version = 15 } ) @@ -920,7 +920,7 @@ test.register_coroutine_test( end, { test_init = test_init_refrigerator, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua index cf69721714..b6c5f3091d 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_microwave_oven.lua @@ -174,7 +174,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -258,7 +258,7 @@ test.register_message_test( test_init() init_supported_microwave_oven_modes() end, - min_api_version = 17 + min_api_version = 15 } ) @@ -338,7 +338,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -453,7 +453,7 @@ test.register_message_test( }, -- on receiving NO ERROR we don't do anything. }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -479,7 +479,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua index 905f34d666..737f67de12 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_oven.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_oven.lua @@ -143,7 +143,7 @@ test.register_coroutine_test( "Cook Surface Two Endpoint must be 6") end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -228,7 +228,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -287,7 +287,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -371,7 +371,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -411,7 +411,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -437,7 +437,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -475,7 +475,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -513,7 +513,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -535,7 +535,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -557,7 +557,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua index 1210819d51..6fce3a45fb 100644 --- a/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua +++ b/drivers/SmartThings/matter-appliance/src/test/test_refrigerator.lua @@ -147,7 +147,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -206,7 +206,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -249,7 +249,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -308,7 +308,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -351,7 +351,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-button/src/test/test_matter_button.lua b/drivers/SmartThings/matter-button/src/test/test_matter_button.lua index dfc32bc7cb..98b46650f0 100644 --- a/drivers/SmartThings/matter-button/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-button/src/test/test_matter_button.lua @@ -71,7 +71,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -109,7 +109,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -196,7 +196,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -268,7 +268,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -308,7 +308,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -348,7 +348,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -373,7 +373,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) -- run the tests diff --git a/drivers/SmartThings/matter-button/src/test/test_matter_button_parent_child.lua b/drivers/SmartThings/matter-button/src/test/test_matter_button_parent_child.lua index 01d3b103c7..a8e1ecd69f 100644 --- a/drivers/SmartThings/matter-button/src/test/test_matter_button_parent_child.lua +++ b/drivers/SmartThings/matter-button/src/test/test_matter_button_parent_child.lua @@ -136,7 +136,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -237,7 +237,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -261,7 +261,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -295,7 +295,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -329,7 +329,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -354,7 +354,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) -- run the tests diff --git a/drivers/SmartThings/matter-button/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-button/src/test/test_matter_multi_button.lua index 91f3a06494..9158f8c6cd 100644 --- a/drivers/SmartThings/matter-button/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-button/src/test/test_matter_multi_button.lua @@ -136,7 +136,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -169,7 +169,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -194,7 +194,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.held({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -243,7 +243,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -283,7 +283,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button4", button_attr.double({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -311,7 +311,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -339,7 +339,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -377,7 +377,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -416,7 +416,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -464,7 +464,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -488,7 +488,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -512,7 +512,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -536,7 +536,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -576,7 +576,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -616,7 +616,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -641,7 +641,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -686,7 +686,7 @@ test.register_message_test( -- no double event }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -744,7 +744,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) -- run the tests diff --git a/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua b/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua index 329daa6bd7..4c899a2042 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_battery_storage.lua @@ -104,7 +104,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) test.register_coroutine_test( @@ -162,7 +162,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -179,7 +179,7 @@ test.register_coroutine_test( capabilities.powerMeter.power({ value = 30.0, unit = "W" }))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -196,7 +196,7 @@ test.register_coroutine_test( capabilities.powerMeter.power({ value = 30.0, unit = "W" }))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( test_init = function() test_init() end, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-energy/src/test/test_evse.lua b/drivers/SmartThings/matter-energy/src/test/test_evse.lua index 5614b0be18..d1b9c92324 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_evse.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_evse.lua @@ -120,7 +120,7 @@ test.register_coroutine_test( "Device Energy Management Endpoint must be 3") end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -150,7 +150,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -201,7 +201,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -225,7 +225,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -248,7 +248,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -271,7 +271,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -294,7 +294,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -317,7 +317,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -340,7 +340,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -364,7 +364,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -438,7 +438,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -541,7 +541,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-energy/src/test/test_evse_energy_meas.lua b/drivers/SmartThings/matter-energy/src/test/test_evse_energy_meas.lua index f3b3b6c700..bf56269261 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_evse_energy_meas.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_evse_energy_meas.lua @@ -118,7 +118,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -152,7 +152,7 @@ test.register_coroutine_test( test_init = function() test_init() end, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua b/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua index 8e4e3c4e9a..f69f034a86 100644 --- a/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua +++ b/drivers/SmartThings/matter-energy/src/test/test_solar_power.lua @@ -124,7 +124,7 @@ test.register_coroutine_test( capabilities.powerMeter.power({ value = 35.0, unit = "W" }))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -172,7 +172,7 @@ test.register_coroutine_test( test_init = function() test_init() end, - min_api_version = 17 + min_api_version = 15 } ) @@ -197,7 +197,7 @@ test.register_coroutine_test( clusters.ElectricalEnergyMeasurement.types.EnergyMeasurementStruct({ energy = 100000, start_timestamp = 0, end_timestamp = 0, start_systime = 0, end_systime = 0, apparent_energy = 0, reactive_energy = 0 })) }) --100Wh end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua b/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua index af8315375f..4c894b7bdb 100644 --- a/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua +++ b/drivers/SmartThings/matter-hrap/src/test/test_thread_border_router_network.lua @@ -108,7 +108,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -143,7 +143,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -191,7 +191,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -230,7 +230,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -356,7 +356,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_aqara_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_aqara_matter_lock.lua index 57d7e2b71e..7c6bc2e783 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_aqara_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_aqara_matter_lock.lua @@ -95,7 +95,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -185,7 +185,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -211,7 +211,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -310,7 +310,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -327,7 +327,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua index 2f3566eb6c..0ce5d58404 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_bridged_matter_lock.lua @@ -91,7 +91,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( mock_device_level:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua index 58185f2ec8..580bd34420 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock.lua @@ -70,7 +70,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -94,7 +94,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -117,7 +117,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -140,7 +140,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -163,7 +163,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -186,7 +186,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -209,7 +209,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -234,7 +234,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -261,7 +261,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -330,7 +330,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -370,7 +370,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -385,7 +385,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua index e43e075baf..9113374514 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_battery.lua @@ -117,7 +117,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "base-lock" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -145,7 +145,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "base-lock-batteryLevel" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -155,7 +155,7 @@ test.register_coroutine_test( end, { test_init = test_init_no_battery, - min_api_version = 17 + min_api_version = 15 } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua index f1b3c2f182..8995332812 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_batteryLevel.lua @@ -95,7 +95,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua index c2e86aae29..4ccc4d135b 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_codes.lua @@ -223,7 +223,7 @@ test.register_coroutine_test( expect_reload_all_codes_messages(mock_device) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -239,7 +239,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -263,7 +263,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -287,7 +287,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -311,7 +311,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -333,7 +333,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -358,7 +358,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -373,7 +373,7 @@ test.register_coroutine_test( expect_reload_all_codes_messages(mock_device) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -418,7 +418,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -456,7 +456,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -562,7 +562,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -611,7 +611,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -671,7 +671,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -706,7 +706,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) test.register_coroutine_test( @@ -740,7 +740,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -782,7 +782,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -825,7 +825,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -904,7 +904,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -971,7 +971,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 15 } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua index 39066776f9..9e39655943 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_cota.lua @@ -143,7 +143,7 @@ test.register_coroutine_test( expect_kick_off_cota_process(mock_device) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -160,7 +160,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -177,7 +177,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -212,7 +212,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -236,7 +236,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -338,7 +338,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -436,7 +436,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -527,7 +527,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_device.id, read_attribute_list}) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -587,7 +587,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -627,7 +627,7 @@ test.register_coroutine_test( test.mock_time.advance_time(2) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -708,7 +708,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -743,7 +743,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -751,7 +751,7 @@ test.register_coroutine_test( "Delay setting COTA cred if another cred is already being set.", function() end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_migration.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_migration.lua index b94b31e105..8e1f731599 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_migration.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_migration.lua @@ -1,143 +1,143 @@ --- 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 clusters = require "st.matter.clusters" -local DoorLock = clusters.DoorLock -local lock_utils = require "lock_utils" - -local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("base-lock.yml"), - manufacturer_info = { - vendor_id = 0x135D, - product_id = 0x00C1, - }, - endpoints = { - { - endpoint_id = 0, - clusters = { - { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, - }, - device_types = { - { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode - } - }, - { - endpoint_id = 1, - clusters = { - { - cluster_id = DoorLock.ID, - cluster_type = "SERVER", - cluster_revision = 1, - feature_map = 0x0101, -- PIN & USR - } - }, - device_types = { - { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock - } - } - } -}) - -local function test_init() - test.disable_startup_messages() - test.mock_device.add_test_device(mock_device) - -- mock_device:set_field(lock_utils.LOCK_CODES_FOR_MIGRATION, { {1, "ST Remote Operation Code"}, {2, "Guest1"}, {3, "Guest2"} }, {persist=true}) -end - -test.set_test_init_function(test_init) - -local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) -subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) -subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) -subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device)) -subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device)) -subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device)) -subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device)) -subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) -subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) -subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) - -local enabled_optional_component_capability_pairs = {{ - "main", - { - capabilities.lockUsers.ID, - capabilities.lockCredentials.ID, - capabilities.battery.ID, - } -}} - -test.register_coroutine_test( - "Migration completed and User and Credential values restored", - function() - test.wait_for_events() - test.socket.capability:__queue_receive( - { - mock_device.id, - {capability = capabilities.lockCodes.ID, command = "migrate", args = {}} - } - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true)) - ) - -- at this point, refresh is injected in migration. - - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) - ) - mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = enabled_optional_component_capability_pairs }) - - - test.wait_for_events() - -- assume this was set prior - mock_device:set_field(lock_utils.LOCK_CODES_FOR_MIGRATION, { {1, "ST Remote Operation Code"}, {2, "Guest1"}, {3, "Guest2"} }, {persist=true}) - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed( - {profile = {id = "00000000-1111-2222-3333-000000000010", components = { main = {capabilities={ - ["lock"]= {id="lock", version=1}, ["lockAlarm"] = {id="lockAlarm", version=1}, ["remoteControlStatus"] = {id="remoteControlStatus", version=1}, - ["lockUsers"] = {id="lockUsers", version=1}, ["lockCredentials"] = {id="lockCredentials", version=1}, ["firmwareUpdate"] = {id="firmwareUpdate", version=1}, - ["refresh"] = {id="refresh", version=1}}}}}} - )) - test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockUsers.users( - { - {userIndex=1, userName="ST Remote Operation Code", userType="guest"}, - {userIndex=2, userName="Guest1", userType="guest"}, - {userIndex=3, userName="Guest2", userType="guest"} - }, - {visibility={displayed=false}}) - ) - ) - test.socket.capability:__expect_send( - mock_device:generate_test_message( - "main", - capabilities.lockCredentials.credentials( - { - {credentialIndex=1, credentialType="pin", userIndex=1}, - {credentialIndex=2, credentialType="pin", userIndex=2}, - {credentialIndex=3, credentialType="pin", userIndex=3} - }, - {visibility={displayed=false}} - ) - ) - ) - end, - { - min_api_version = 17 - } -) - -test.run_registered_tests() +-- 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 clusters = require "st.matter.clusters" +local DoorLock = clusters.DoorLock +local lock_utils = require "lock_utils" + +local mock_device = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("base-lock.yml"), + manufacturer_info = { + vendor_id = 0x135D, + product_id = 0x00C1, + }, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.BasicInformation.ID, cluster_type = "SERVER" }, + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = 1, + clusters = { + { + cluster_id = DoorLock.ID, + cluster_type = "SERVER", + cluster_revision = 1, + feature_map = 0x0101, -- PIN & USR + } + }, + device_types = { + { device_type_id = 0x000A, device_type_revision = 1 } -- Door Lock + } + } + } +}) + +local function test_init() + test.disable_startup_messages() + test.mock_device.add_test_device(mock_device) + -- mock_device:set_field(lock_utils.LOCK_CODES_FOR_MIGRATION, { {1, "ST Remote Operation Code"}, {2, "Guest1"}, {3, "Guest2"} }, {persist=true}) +end + +test.set_test_init_function(test_init) + +local subscribe_request = DoorLock.attributes.LockState:subscribe(mock_device) +subscribe_request:merge(DoorLock.attributes.OperatingMode:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfTotalUsersSupported:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.NumberOfPINUsersSupported:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.MaxPINCodeLength:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.MinPINCodeLength:subscribe(mock_device)) +subscribe_request:merge(DoorLock.attributes.RequirePINforRemoteOperation:subscribe(mock_device)) +subscribe_request:merge(DoorLock.events.LockOperation:subscribe(mock_device)) +subscribe_request:merge(DoorLock.events.DoorLockAlarm:subscribe(mock_device)) +subscribe_request:merge(DoorLock.events.LockUserChange:subscribe(mock_device)) + +local enabled_optional_component_capability_pairs = {{ + "main", + { + capabilities.lockUsers.ID, + capabilities.lockCredentials.ID, + capabilities.battery.ID, + } +}} + +test.register_coroutine_test( + "Migration completed and User and Credential values restored", + function() + test.wait_for_events() + test.socket.capability:__queue_receive( + { + mock_device.id, + {capability = capabilities.lockCodes.ID, command = "migrate", args = {}} + } + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockCodes.migrated(true)) + ) + -- at this point, refresh is injected in migration. + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockValues({"locked", "unlocked", "not fully locked"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lock.supportedLockCommands({"lock", "unlock"}, {visibility = {displayed = false}})) + ) + mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = enabled_optional_component_capability_pairs }) + + + test.wait_for_events() + -- assume this was set prior + mock_device:set_field(lock_utils.LOCK_CODES_FOR_MIGRATION, { {1, "ST Remote Operation Code"}, {2, "Guest1"}, {3, "Guest2"} }, {persist=true}) + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed( + {profile = {id = "00000000-1111-2222-3333-000000000010", components = { main = {capabilities={ + ["lock"]= {id="lock", version=1}, ["lockAlarm"] = {id="lockAlarm", version=1}, ["remoteControlStatus"] = {id="remoteControlStatus", version=1}, + ["lockUsers"] = {id="lockUsers", version=1}, ["lockCredentials"] = {id="lockCredentials", version=1}, ["firmwareUpdate"] = {id="firmwareUpdate", version=1}, + ["refresh"] = {id="refresh", version=1}}}}}} + )) + test.socket["matter"]:__expect_send({mock_device.id, subscribe_request}) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.alarm.clear({state_change = true})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockUsers.users( + { + {userIndex=1, userName="ST Remote Operation Code", userType="guest"}, + {userIndex=2, userName="Guest1", userType="guest"}, + {userIndex=3, userName="Guest2", userType="guest"} + }, + {visibility={displayed=false}}) + ) + ) + test.socket.capability:__expect_send( + mock_device:generate_test_message( + "main", + capabilities.lockCredentials.credentials( + { + {credentialIndex=1, credentialType="pin", userIndex=1}, + {credentialIndex=2, credentialType="pin", userIndex=2}, + {credentialIndex=3, credentialType="pin", userIndex=3} + }, + {visibility={displayed=false}} + ) + ) + ) + end, + { + min_api_version = 15 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua index 745e769f9e..ca1aedfb04 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_modular.lua @@ -361,7 +361,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = {{"main", {"batteryLevel"}}} }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -396,7 +396,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "lock-modular", optional_component_capabilities = {{"main", {"battery"}}} }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -431,7 +431,7 @@ test.register_coroutine_test( end, { test_init = test_init_unlatch, - min_api_version = 17 + min_api_version = 15 } ) @@ -467,7 +467,7 @@ test.register_coroutine_test( end, { test_init = test_init_unlatch, - min_api_version = 17 + min_api_version = 15 } ) @@ -503,7 +503,7 @@ test.register_coroutine_test( end, { test_init = test_init_user_pin, - min_api_version = 17 + min_api_version = 15 } ) @@ -540,7 +540,7 @@ test.register_coroutine_test( end, { test_init = test_init_user_pin_schedule_unlatch, - min_api_version = 17 + min_api_version = 15 } ) @@ -574,7 +574,10 @@ test.register_coroutine_test( ) mock_device_modular:expect_metadata_update({ profile = "lock-modular-embedded-unlatch", optional_component_capabilities = {{"main", {"lockUsers", "lockCredentials", "lockSchedules", "battery"}}} }) end, - { test_init = test_init_modular } + { + test_init = test_init_modular, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -582,7 +585,10 @@ test.register_coroutine_test( -- simulate no actual change test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({})) end, - { test_init = test_init_modular } + { + test_init = test_init_modular, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -619,7 +625,7 @@ test.register_coroutine_test( end, { test_init = test_init_modular, - min_api_version = 17 + min_api_version = 15 } ) @@ -692,7 +698,7 @@ test.register_coroutine_test( test.mock_device.add_test_device(mock_nuki_smart_lock_ultra) mock_nuki_smart_lock_ultra:set_field(profiling_data.BATTERY_SUPPORT, battery_support.BATTERY_PERCENTAGE, {persist = true}) -- assume this has been set previously end, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua index f040c2b26e..f2224af358 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_matter_lock_unlatch.lua @@ -106,7 +106,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -178,7 +178,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -226,7 +226,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -248,7 +248,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -270,7 +270,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -292,7 +292,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -381,7 +381,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua index 35acbfbff6..0054d552ea 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock.lua @@ -96,7 +96,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -184,7 +184,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -224,7 +224,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -264,7 +264,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -284,7 +284,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -304,7 +304,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -375,7 +375,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -456,7 +456,7 @@ function() test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -473,7 +473,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -493,7 +493,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -512,7 +512,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -533,7 +533,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -557,7 +557,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -579,7 +579,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -601,7 +601,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -623,7 +623,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -649,7 +649,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -748,7 +748,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -975,7 +975,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1044,7 +1044,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1071,7 +1071,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1115,7 +1115,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1164,7 +1164,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1191,7 +1191,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1257,7 +1257,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1284,7 +1284,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1350,7 +1350,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1377,7 +1377,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1442,7 +1442,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1471,7 +1471,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1527,7 +1527,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1590,7 +1590,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1646,7 +1646,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1794,7 +1794,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1823,7 +1823,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1877,7 +1877,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1906,7 +1906,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1978,7 +1978,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2007,7 +2007,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2078,7 +2078,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2107,7 +2107,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2157,7 +2157,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2189,7 +2189,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2257,7 +2257,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2286,7 +2286,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2338,7 +2338,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2370,7 +2370,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2501,7 +2501,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2609,7 +2609,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua index 250fcb8af5..573dbf0e62 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_aliro.lua @@ -155,7 +155,10 @@ test.register_coroutine_test( capabilities.lockAliro.readerVerificationKey(EXPECTED_PUB_HEX, {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -176,7 +179,10 @@ test.register_coroutine_test( capabilities.lockAliro.readerGroupIdentifier(EXPECTED_GROUP_ID_HEX, {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -199,7 +205,10 @@ test.register_coroutine_test( {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -222,7 +231,10 @@ test.register_coroutine_test( {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -245,7 +257,10 @@ test.register_coroutine_test( {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -266,7 +281,10 @@ test.register_coroutine_test( capabilities.lockAliro.groupResolvingKey(EXPECTED_GROUP_ID_HEX, {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -289,7 +307,10 @@ test.register_coroutine_test( {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -312,7 +333,10 @@ test.register_coroutine_test( {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -334,7 +358,10 @@ test.register_coroutine_test( capabilities.lockAliro.cardId("3icub18c8pr00", {visibility = {displayed = false}}) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -386,7 +413,10 @@ test.register_coroutine_test( ) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -546,7 +576,10 @@ test.register_coroutine_test( ) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -585,7 +618,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -660,7 +693,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -730,7 +763,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -800,7 +833,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -957,7 +990,10 @@ test.register_coroutine_test( ) ) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -993,7 +1029,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1066,7 +1102,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1133,7 +1169,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1200,7 +1236,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1229,7 +1265,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -1257,7 +1296,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.lockAlarm.supportedAlarmValues({"unableToLockTheDoor"}, {visibility = {displayed = false}})) ) - end + end, + { + min_api_version = 15 + } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua index 8ec2a2a781..a13f3d455c 100644 --- a/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua +++ b/drivers/SmartThings/matter-lock/src/test/test_new_matter_lock_battery.lua @@ -427,7 +427,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "lock" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -458,7 +458,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "lock-batteryLevel" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -490,7 +490,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "lock-battery" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -521,7 +521,7 @@ test.register_coroutine_test( end, { test_init = test_init_unlatch, - min_api_version = 17 + min_api_version = 14 } ) @@ -553,7 +553,7 @@ test.register_coroutine_test( end, { test_init = test_init_unlatch, - min_api_version = 17 + min_api_version = 14 } ) @@ -586,7 +586,7 @@ test.register_coroutine_test( end, { test_init = test_init_unlatch, - min_api_version = 17 + min_api_version = 14 } ) @@ -617,7 +617,7 @@ test.register_coroutine_test( end, { test_init = test_init_user_pin, - min_api_version = 17 + min_api_version = 14 } ) @@ -649,7 +649,7 @@ test.register_coroutine_test( end, { test_init = test_init_user_pin, - min_api_version = 17 + min_api_version = 14 } ) @@ -682,7 +682,7 @@ test.register_coroutine_test( end, { test_init = test_init_user_pin, - min_api_version = 17 + min_api_version = 14 } ) @@ -713,7 +713,7 @@ test.register_coroutine_test( end, { test_init = test_init_user_pin_schedule_unlatch, - min_api_version = 17 + min_api_version = 14 } ) @@ -745,7 +745,7 @@ test.register_coroutine_test( end, { test_init = test_init_user_pin_schedule_unlatch, - min_api_version = 17 + min_api_version = 14 } ) @@ -778,7 +778,7 @@ test.register_coroutine_test( end, { test_init = test_init_user_pin_schedule_unlatch, - min_api_version = 17 + min_api_version = 14 } ) @@ -799,7 +799,7 @@ test.register_coroutine_test( end, { test_init = test_init_battery, - min_api_version = 17 + min_api_version = 14 } ) @@ -842,7 +842,7 @@ test.register_coroutine_test( end, { test_init = test_init_battery_level, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua b/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua index b795f6438a..85d8d8868b 100644 --- a/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua +++ b/drivers/SmartThings/matter-media/src/test/test_matter_media_speaker.lua @@ -126,7 +126,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -167,7 +167,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -213,7 +213,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -335,7 +335,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -366,7 +366,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua b/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua index dc3a4e4c08..c94b63275a 100644 --- a/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua +++ b/drivers/SmartThings/matter-media/src/test/test_matter_media_video_player.lua @@ -240,7 +240,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -278,7 +278,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -316,7 +316,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -354,7 +354,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -395,7 +395,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -436,7 +436,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -605,7 +605,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -638,7 +638,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -671,7 +671,7 @@ test.register_coroutine_test( mock_device_variable_speed:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua index 626f7fea60..e1c95249a8 100644 --- a/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua +++ b/drivers/SmartThings/matter-rvc/src/test/test_matter_rvc.lua @@ -227,7 +227,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -243,7 +243,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -274,7 +274,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -302,7 +302,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -330,7 +330,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -357,7 +357,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -378,7 +378,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -419,7 +419,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -457,7 +457,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -521,7 +521,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -575,7 +575,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -632,7 +632,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -689,7 +689,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -743,7 +743,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -797,7 +797,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1032,7 +1032,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1068,7 +1068,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1094,7 +1094,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1120,7 +1120,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -1205,7 +1205,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor.lua index 8d3bd462a5..781854ee8b 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor.lua @@ -499,7 +499,7 @@ test.register_coroutine_test( test_aqs_device_type_do_configure(mock_device, "aqs-temp-humidity-all-level-all-meas") end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -510,7 +510,7 @@ test.register_coroutine_test( end, { test_init = test_init_common, - min_api_version = 17 + min_api_version = 15 } ) @@ -521,7 +521,7 @@ test.register_coroutine_test( end, { test_init = test_init_level, - min_api_version = 17 + min_api_version = 15 } ) @@ -533,7 +533,7 @@ test.register_coroutine_test( end, { test_init = test_init_co_co2, - min_api_version = 17 + min_api_version = 15 } ) @@ -544,7 +544,7 @@ test.register_coroutine_test( end, { test_init = test_init_tvoc, - min_api_version = 17 + min_api_version = 15 } ) @@ -568,7 +568,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -590,7 +590,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -625,7 +625,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -695,7 +695,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -720,7 +720,7 @@ test.register_coroutine_test( end, { test_init = test_init_common, - min_api_version = 17 + min_api_version = 14 } ) @@ -828,7 +828,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua index 27921e0dec..4a18304d56 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_air_quality_sensor_modular.lua @@ -353,6 +353,7 @@ test.register_coroutine_test( end, { test_init = test_init_modular_fingerprint, + min_api_version = 15 } ) @@ -409,7 +410,7 @@ test.register_coroutine_test( end, { test_init = test_init_all, - min_api_version = 17 + min_api_version = 15 } ) @@ -439,7 +440,7 @@ test.register_coroutine_test( end, { test_init = test_init_common, - min_api_version = 17 + min_api_version = 15 } ) @@ -466,7 +467,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device_modular_fingerprint:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, {visibility={displayed=false}}))) test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, subscribe_request_tvoc}) end, - { test_init = test_init_modular_fingerprint } + { + test_init = test_init_modular_fingerprint, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -475,7 +479,10 @@ test.register_coroutine_test( -- simulate no actual change test.socket.device_lifecycle:__queue_receive(mock_device_modular_fingerprint:generate_info_changed({})) end, - { test_init = test_init_modular_fingerprint } + { + test_init = test_init_modular_fingerprint, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -501,7 +508,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device_modular_fingerprint:generate_test_message("main", capabilities.airQualityHealthConcern.supportedAirQualityValues({"unknown", "good", "unhealthy", "moderate", "slightlyUnhealthy"}, {visibility={displayed=false}}))) test.socket.matter:__expect_send({mock_device_modular_fingerprint.id, subscribe_request_tvoc}) end, - { test_init = test_init_modular_fingerprint } + { + test_init = test_init_modular_fingerprint, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -510,7 +520,10 @@ test.register_coroutine_test( -- simulate no actual change test.socket.device_lifecycle:__queue_receive(mock_device_modular_fingerprint:generate_info_changed({})) end, - { test_init = test_init_modular_fingerprint } + { + test_init = test_init_modular_fingerprint, + min_api_version = 15 + } ) -- run tests diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua index 7f384c744e..ba4f2fb296 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua @@ -61,7 +61,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -77,7 +77,7 @@ test.register_coroutine_test( assert(mock_device.profile.id == current_profile, "Profile should not change on driverSwitched") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -134,7 +134,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua index 644c4088e1..d592a9089e 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_bosch_button_contact.lua @@ -82,7 +82,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -232,7 +232,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -257,7 +257,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -292,7 +292,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_flow_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_flow_sensor.lua index dd4eb87252..952989f6a1 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_flow_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_flow_sensor.lua @@ -71,7 +71,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua index 02e5815c06..5cd5e85950 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_freeze_leak_sensor.lua @@ -99,7 +99,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -135,7 +135,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -170,7 +170,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -191,7 +191,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -211,7 +211,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -244,7 +244,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua index 13bd93ed70..8920bfcf7f 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_pressure_sensor.lua @@ -94,7 +94,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua index 2b942c6f0c..6e8c11099d 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_rain_sensor.lua @@ -96,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -131,7 +131,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua index d6024f61be..8e9b8580d9 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor.lua @@ -163,7 +163,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -185,7 +185,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -207,7 +207,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -242,7 +242,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -264,7 +264,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -299,7 +299,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -336,7 +336,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -366,7 +366,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -381,7 +381,7 @@ test.register_coroutine_test( end, { test_init = test_init_presence_sensor, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_battery.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_battery.lua index 0709be6a31..7393937682 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_battery.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_battery.lua @@ -88,7 +88,7 @@ test.register_coroutine_test( mock_device_humidity_battery:expect_metadata_update({ profile = "humidity-battery" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -116,7 +116,7 @@ test.register_coroutine_test( mock_device_humidity_battery:expect_metadata_update({ profile = "humidity-batteryLevel" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -142,7 +142,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua index 0a02bbb27f..df9ab678cb 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_featuremap.lua @@ -171,7 +171,7 @@ test.register_coroutine_test( end, { test_init = test_init_humidity_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -181,7 +181,7 @@ test.register_coroutine_test( end, { test_init = test_init_humidity_no_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -191,7 +191,7 @@ test.register_coroutine_test( end, { test_init = test_init_temp_humidity, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua index 6ca73351c9..1093c1be76 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_sensor_rpc.lua @@ -71,7 +71,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua index aeef7b15f9..2170115366 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm.lua @@ -124,7 +124,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -172,7 +172,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -220,7 +220,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -281,7 +281,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -342,7 +342,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -390,7 +390,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -435,7 +435,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -470,7 +470,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -492,7 +492,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -527,7 +527,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -578,7 +578,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua index 3c8102064e..1b96842797 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_smoke_co_alarm_battery.lua @@ -92,7 +92,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "smoke-co-temp-humidity-comeas-battery" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -107,7 +107,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -127,7 +127,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua index a5a8042fc0..dbdc25527e 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_soil_sensor.lua @@ -80,7 +80,10 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "Relative humidity reports should generate correct messages", @@ -105,7 +108,10 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 41 })) ) end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "Temperature reports should generate correct messages", @@ -120,7 +126,10 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 40.0, unit = "C" })) ) end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "Min and max temperature attributes set capability constraint", @@ -144,7 +153,10 @@ test.register_coroutine_test( ) ) end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "Soil moisture is reported raw when no limits are set", @@ -159,7 +171,10 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 55 })) ) end -) +, + { + min_api_version = 15 + }) local function build_soil_moisture_limits(min_value, max_value) if version.api < 21 then @@ -194,7 +209,10 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 25 })) ) end -) +, + { + min_api_version = 17 + }) test.register_coroutine_test( "Soil moisture scaling rounds correctly", @@ -224,6 +242,9 @@ test.register_coroutine_test( mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 90 })) ) end -) +, + { + min_api_version = 17 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua index 50713626f2..0e92647a4a 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_climate_sensor_w100.lua @@ -160,7 +160,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -185,7 +185,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -213,7 +213,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -232,7 +232,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -257,7 +257,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -282,7 +282,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -323,7 +323,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(aqara_mock_device:generate_test_message("button2", capabilities.button.button.double({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -352,7 +352,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -381,7 +381,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -400,7 +400,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -419,7 +419,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -444,7 +444,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -467,7 +467,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -490,7 +490,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua index dbda0c35fc..ad197cf435 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_cube.lua @@ -235,7 +235,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -270,7 +270,7 @@ test.register_coroutine_test( end, { test_init = test_init_exhausted, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua index 468232cbc3..08402cf273 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_aqara_light_switch_h2.lua @@ -263,7 +263,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -343,7 +343,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua index 18c44f7b9c..32989187fa 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_set.lua @@ -227,7 +227,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -295,7 +295,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -320,7 +320,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -382,7 +382,7 @@ test.register_coroutine_test( end, { test_init = test_init_periodic, - min_api_version = 17 + min_api_version = 14 } ) @@ -407,7 +407,7 @@ test.register_coroutine_test( end, { test_init = test_init, - min_api_version = 17 + min_api_version = 14 } ) @@ -422,7 +422,7 @@ test.register_coroutine_test( end, { test_init = test_init_periodic, - min_api_version = 17 + min_api_version = 14 } ) @@ -553,7 +553,7 @@ test.register_coroutine_test( end, { test_init = test_init, - min_api_version = 17 + min_api_version = 14 } ) @@ -624,7 +624,7 @@ test.register_coroutine_test( end, { test_init = test_init_periodic, - min_api_version = 17 + min_api_version = 14 } ) @@ -707,7 +707,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua index 2abfe6e9c5..e285c593e2 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_electrical_sensor_tree.lua @@ -163,7 +163,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -231,7 +231,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -257,7 +257,7 @@ test.register_coroutine_test( end, { test_init = test_init, - min_api_version = 17 + min_api_version = 14 } ) @@ -340,7 +340,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua index e90f5a70da..f51143b813 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_eve_energy.lua @@ -197,7 +197,7 @@ test.register_coroutine_test( assert(sub_driver ~= nil, "sub_driver should be returned") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -209,7 +209,7 @@ test.register_coroutine_test( assert(result == false, "can_handle should return false for device without Eve Private Cluster") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -242,7 +242,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -295,7 +295,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -310,7 +310,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -343,7 +343,7 @@ test.register_coroutine_test( test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end, - min_api_version = 17 + min_api_version = 14 } ) @@ -362,7 +362,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -389,7 +389,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -416,7 +416,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -455,7 +455,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -483,7 +483,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -510,7 +510,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -546,7 +546,7 @@ test.register_coroutine_test( test.timer.__create_and_queue_test_time_advance_timer(60 * 15, "interval", "create_poll_report_schedule") test.timer.__create_and_queue_test_time_advance_timer(60, "interval", "create_poll_schedule") end, - min_api_version = 17 + min_api_version = 14 } ) @@ -643,7 +643,7 @@ test.register_coroutine_test( test.disable_startup_messages() test.mock_device.add_test_device(mock_eve_device_using_electrical_sensor) end, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua index 96a684529d..a85137d0e3 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_hager_waasys.lua @@ -592,7 +592,10 @@ test.register_coroutine_test("Test: 4-Button Device Detection - Profile Changes })) }) -end) +end, + { + min_api_version = 15 + }) 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") @@ -796,7 +799,10 @@ test.register_coroutine_test("Test: Button Event Handling - Pushed, Double Press ) }) test.socket.capability:__expect_send(matter_device:generate_test_message("button4", capabilities.button.button.held({ state_change = true }))) -end) +end, + { + min_api_version = 15 + }) 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") @@ -889,7 +895,10 @@ test.register_coroutine_test("Test: Device Type Handler - Handles Button (Type 1 parent.id, clusters.OnOff.attributes.OnOff:subscribe(parent, nil) }) -end) +end, + { + min_api_version = 15 + }) 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") @@ -1131,7 +1140,10 @@ test.register_coroutine_test("Test: 2G Relay - Profile Changes Between light-bin parent_device_id = parent.id, parent_assigned_child_key = "4" }) -end) +end, + { + min_api_version = 15 + }) test.register_coroutine_test("Test: Dimmer Device - Child Creation for Dimmable Endpoint with Button Support", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1227,7 +1239,10 @@ test.register_coroutine_test("Test: Dimmer Device - Child Creation for Dimmable clusters.LevelControl.server.commands.MoveToLevelWithOnOff(child_dimmer, 3, 51, nil, 0, 0) }) -end) +end, + { + min_api_version = 15 + }) test.register_coroutine_test("Test: 1G Dimmer - Initialization and Profile Update to light-level", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1236,7 +1251,10 @@ test.register_coroutine_test("Test: 1G Dimmer - Initialization and Profile Updat add_matter_device(dimmer, parent, "2-button") test.wait_for_events() configure_parent(parent) -end) +end, + { + min_api_version = 15 + }) test.register_coroutine_test("Test: 1G Dimmer - Host Commands and Level Control Capabilities", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1308,7 +1326,10 @@ test.register_coroutine_test("Test: 1G Dimmer - Host Commands and Level Control parent_1g.id, clusters.LevelControl.server.commands.MoveToLevelWithOnOff(dimmer, 4, 51, nil, 0, 0) }) -end) +end, + { + min_api_version = 15 + }) test.register_coroutine_test("Test: PIR Device - Initialization with Motion and Illuminance Capabilities", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1318,7 +1339,10 @@ test.register_coroutine_test("Test: PIR Device - Initialization with Motion and test.wait_for_events() configure_parent(parent_pir) -end) +end, + { + min_api_version = 15 + }) test.register_coroutine_test("Test: PIR Device - Complete Functionality with Motion, Illuminance, and Dimmer Support", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1472,7 +1496,10 @@ test.register_coroutine_test("Test: PIR Device - Complete Functionality with Mot }) 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) +end, + { + min_api_version = 15 + }) 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") @@ -1610,7 +1637,10 @@ test.register_coroutine_test("Test: Host with Window Covering - 2-Button Profile 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) +end, + { + min_api_version = 15 + }) test.register_coroutine_test("Test: Window Covering - Preference Changes for Reverse Polarity and Preset Position", function() test.socket.matter:__set_channel_ordering("relaxed") @@ -1775,6 +1805,9 @@ test.register_coroutine_test("Test: Window Covering - Preference Changes for Rev parent.id, cluster_base.subscribe(parent, nil, clusters.WindowCovering.ID, clusters.WindowCovering.attributes.CurrentPositionLiftPercent100ths.ID, nil) }) -end) +end, + { + min_api_version = 15 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua index be28bd040a..6b0bfe32a1 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_ikea_scroll.lua @@ -233,7 +233,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -347,7 +347,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -461,7 +461,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -512,7 +512,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -563,7 +563,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -614,7 +614,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -665,7 +665,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -699,7 +699,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -733,7 +733,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -747,7 +747,10 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_ikea_scroll.id, read_request}) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- Coroutine tests for sensitivity preference logic. -- KNOB_SENSITIVITY_FACTORS = {0.5, 1.0, 2.0}, indexed by the preference value. @@ -797,7 +800,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -845,7 +848,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -893,7 +896,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -941,7 +944,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua index b22498eaf7..efc80d9e99 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua @@ -78,7 +78,10 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_device.id, clusters.ColorControl.attributes.Options:write(mock_device, 1, clusters.ColorControl.types.OptionsBitmap.EXECUTE_IF_OFF)}) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "init should cause device to subscribe to all appropriate clusters", function() @@ -109,7 +112,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -131,7 +134,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -166,7 +169,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -245,7 +248,7 @@ test.register_coroutine_test( end, { test_init = test_init_light_level_motion, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua index 6cb679796d..eda33fbf5b 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_bridge.lua @@ -101,7 +101,7 @@ test.register_coroutine_test( end, { test_init = test_init_mock_bridge, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua index 061f2e2f47..d49d9f2452 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_button.lua @@ -87,7 +87,7 @@ test.register_coroutine_test( end, { test_init = test_init_for_lifecycle_tests, - min_api_version = 17 + min_api_version = 15 } ) @@ -109,7 +109,7 @@ test.register_coroutine_test( end, { test_init = test_init_for_lifecycle_tests, - min_api_version = 17 + min_api_version = 15 } ) @@ -132,7 +132,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -170,7 +170,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -209,7 +209,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -257,7 +257,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -281,7 +281,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -305,7 +305,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -329,7 +329,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -368,7 +368,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -407,7 +407,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -482,7 +482,7 @@ test.register_coroutine_test( end, { test_init = test_init_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -509,7 +509,7 @@ test.register_coroutine_test( end, { test_init = test_init_profile_change_with_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -521,7 +521,7 @@ test.register_coroutine_test( end, { test_init = test_init_profile_change_with_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -536,7 +536,7 @@ test.register_coroutine_test( end, { test_init = test_init_profile_change_with_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -551,7 +551,7 @@ test.register_coroutine_test( end, { test_init = test_init_profile_change_with_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -571,7 +571,7 @@ test.register_coroutine_test( end, { test_init = test_init_profile_change_with_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -594,7 +594,7 @@ test.register_coroutine_test( end, { test_init = test_init_battery, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index c238bfd2ca..3926b63fe9 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -1,12 +1,19 @@ -- Copyright © 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local version = require "version" +local test = require "integration_test" +if version.api < 17 then + -- Run an empty test suite to avoid an error being thrown + test.run_registered_tests() + os.exit(0) +end + local capabilities = require "st.capabilities" local cluster_base = require "st.matter.cluster_base" local clusters = require "st.matter.clusters" local camera_fields = require "sub_drivers.camera.camera_utils.fields" local t_utils = require "integration_test.utils" -local test = require "integration_test" local uint32 = require "st.matter.data_types.Uint32" test.disable_startup_messages() @@ -666,7 +673,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -732,7 +739,7 @@ test.register_coroutine_test( assert(not configure_buttons_called, "configure_buttons should not be called when capability state is unchanged") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -759,7 +766,7 @@ test.register_coroutine_test( assert(reconcile_called, "reconcile_profile_and_capabilities should be called") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -807,7 +814,7 @@ test.register_coroutine_test( assert(init_event_count == 0, "cameraPrivacyMode should not be reinitialized for equal values with metatable differences") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -857,7 +864,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -900,7 +907,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -929,7 +936,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -973,7 +980,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1004,7 +1011,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1054,7 +1061,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1079,7 +1086,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1132,7 +1139,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1256,7 +1263,7 @@ test.register_coroutine_test( emit_supported_resolutions() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1274,7 +1281,7 @@ test.register_coroutine_test( emit_supported_resolutions() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1292,7 +1299,7 @@ test.register_coroutine_test( emit_supported_resolutions() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1347,7 +1354,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1371,7 +1378,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1389,7 +1396,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1407,7 +1414,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1455,7 +1462,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1493,7 +1500,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1519,7 +1526,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1548,7 +1555,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.sounds.selectedSound(2))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1591,7 +1598,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1632,7 +1639,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1658,7 +1665,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1698,7 +1705,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1734,7 +1741,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1759,7 +1766,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1826,7 +1833,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1934,7 +1941,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1960,7 +1967,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1985,7 +1992,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2017,7 +2024,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2098,7 +2105,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2130,7 +2137,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2155,7 +2162,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2229,7 +2236,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2302,7 +2309,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2378,7 +2385,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2470,7 +2477,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2576,7 +2583,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2652,7 +2659,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -3263,7 +3270,7 @@ test.register_coroutine_test( camera_cfg.reconcile_profile_and_capabilities = original_reconcile end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -3304,7 +3311,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_device.id, clusters.Switch.attributes.MultiPressMax:read(mock_device, DOORBELL_EP)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -3316,7 +3323,7 @@ test.register_coroutine_test( end, { test_init = test_init_no_per_zone_sensitivity, - min_api_version = 17 + min_api_version = 14 } ) @@ -3328,7 +3335,7 @@ test.register_coroutine_test( end, { test_init = test_init_no_user_defined_zone, - min_api_version = 17 + min_api_version = 14 } ) @@ -3347,7 +3354,7 @@ test.register_coroutine_test( end, { test_init = test_init_no_user_defined_zone, - min_api_version = 17 + min_api_version = 14 } ) @@ -3364,7 +3371,7 @@ test.register_coroutine_test( end, { test_init = test_init_no_user_defined_zone, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua index 3acf4129f9..31795f91ea 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_irrigation_system.lua @@ -1,6 +1,14 @@ -- Copyright © 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local version = require "version" +local test = require "integration_test" +if version.api < 17 then + test.run_registered_tests() + os.exit(0) +end + + local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua index 32023a36cb..d3be60f352 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_light_fan.lua @@ -157,7 +157,10 @@ test.register_coroutine_test( ) test.socket.matter:__expect_send({mock_device_capabilities_disabled.id, subscribe_request}) end, - { test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end } + { + test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -165,7 +168,10 @@ test.register_coroutine_test( -- simulate no actual change test.socket.device_lifecycle:__queue_receive(mock_device_capabilities_disabled:generate_info_changed({})) end, - { test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end } + { + test_init = function() test.mock_device.add_test_device(mock_device_capabilities_disabled) end, + min_api_version = 15 + } ) local FanMode = clusters.FanControl.attributes.FanMode @@ -213,7 +219,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -249,7 +255,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -274,7 +280,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -299,7 +305,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua index d7d5efde97..0f4c1e28b1 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button.lua @@ -278,7 +278,7 @@ test.register_coroutine_test( end, { test_init = test_init_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -302,7 +302,7 @@ test.register_coroutine_test( end, { test_init = test_init_battery, - min_api_version = 17 + min_api_version = 15 } ) @@ -325,7 +325,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -358,7 +358,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -383,7 +383,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.held({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -408,7 +408,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -432,7 +432,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -472,7 +472,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button5", button_attr.double({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -500,7 +500,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -528,7 +528,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -566,7 +566,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -605,7 +605,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -653,7 +653,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -677,7 +677,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -701,7 +701,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -725,7 +725,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -764,7 +764,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -803,7 +803,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -827,7 +827,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -871,7 +871,7 @@ test.register_message_test( -- no double event }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -929,7 +929,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -951,7 +951,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -972,7 +972,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "5-button-batteryLevel" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -993,7 +993,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "5-button-battery" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua index 44ee11861e..ec2e7ae289 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_motion.lua @@ -184,7 +184,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -217,7 +217,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -242,7 +242,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button2", button_attr.held({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -267,7 +267,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -291,7 +291,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -331,7 +331,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button6", button_attr.double({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -359,7 +359,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -378,7 +378,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -406,7 +406,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -444,7 +444,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -483,7 +483,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -531,7 +531,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -555,7 +555,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -579,7 +579,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -603,7 +603,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -643,7 +643,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -683,7 +683,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -707,7 +707,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -751,7 +751,7 @@ test.register_message_test( -- no double event }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -809,7 +809,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) -- run the tests diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua index 70f9666542..b3390edc29 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_multi_button_switch_mcd.lua @@ -271,7 +271,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -294,7 +294,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -327,7 +327,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -352,7 +352,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("button3", button_attr.pushed({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -411,7 +411,7 @@ test.register_coroutine_test( end, { test_init = test_init_mcd_unsupported_switch_device_type, - min_api_version = 17 + min_api_version = 15 } ) @@ -432,7 +432,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -450,7 +450,7 @@ test.register_coroutine_test( expect_configure_buttons() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -464,7 +464,7 @@ test.register_coroutine_test( expect_configure_buttons() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -484,7 +484,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive({ mock_child.id, "doConfigure" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua index d27ee2c383..fc729ed3fa 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_device_configuration.lua @@ -50,7 +50,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_device_onoff_switch_as_server) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -83,7 +83,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_device_onoff_switch_as_client) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -122,7 +122,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_device_dimmer_switch_as_server) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_device_plug_with_switch_profile_vendor_override) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -196,7 +196,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_device_color_dimmer) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -241,7 +241,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_device_mounted_on_off_control) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -296,7 +296,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_device_mounted_dimmable_load_control) end, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua index 608112d61c..02a01dc44e 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_on_off_parent_child.lua @@ -177,7 +177,7 @@ test.register_coroutine_test( end, { test_init = test_init_for_lifecycle_tests, - min_api_version = 17 + min_api_version = 15 } ) @@ -193,7 +193,7 @@ test.register_coroutine_test( end, { test_init = test_init_for_lifecycle_tests, - min_api_version = 17 + min_api_version = 15 } ) @@ -207,7 +207,7 @@ test.register_coroutine_test( end, { test_init = test_init_for_generate_info_changed_tests, - min_api_version = 17 + min_api_version = 15 } ) @@ -238,7 +238,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -289,7 +289,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -320,6 +320,9 @@ test.register_message_test( clusters.ColorControl.server.commands.MoveToColor:build_test_command_response(mock_device, extended_color_ep_id) } }, + }, + { + min_api_version = 15 } ) @@ -354,7 +357,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -460,7 +463,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -514,7 +517,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -613,7 +616,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -692,7 +695,7 @@ test.register_coroutine_test( test_init = function() test.mock_device.add_test_device(mock_plug) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -710,7 +713,7 @@ test.register_coroutine_test( test_init = function() test.mock_device.add_test_device(mock_plug) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -771,7 +774,7 @@ test.register_coroutine_test( test_init = function() test.mock_device.add_test_device(mock_plug_profile_override) end, - min_api_version = 17 + min_api_version = 15 } ) @@ -871,7 +874,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_switch) end, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua index eabc9246f7..e6d51d1f36 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_sensor_offset_preferences.lua @@ -61,7 +61,7 @@ test.register_coroutine_test("Read appropriate attribute values after tempOffset }))) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -84,7 +84,7 @@ test.register_coroutine_test("Read appropriate attribute values after humidityOf }))) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua index e68f1d0101..d48a89b9cc 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -93,7 +93,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_coroutine_test( test.socket.matter:__expect_send({mock_device.id, subscribe_request}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -143,7 +143,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -291,7 +291,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -316,7 +316,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -346,7 +346,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -376,7 +376,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -448,7 +448,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -526,7 +526,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -551,7 +551,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -576,7 +576,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -606,7 +606,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -631,7 +631,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -676,7 +676,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -694,7 +694,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -759,7 +759,7 @@ test.register_coroutine_test( end, { test_init = test_init_x_y_color_mode, - min_api_version = 17 + min_api_version = 14 } ) @@ -791,7 +791,7 @@ test.register_coroutine_test( end, { test_init = test_init_x_y_color_mode, - min_api_version = 17 + min_api_version = 14 } ) @@ -823,7 +823,7 @@ test.register_coroutine_test( end, { test_init = test_init_x_y_color_mode, - min_api_version = 17 + min_api_version = 14 } ) @@ -881,7 +881,7 @@ test.register_coroutine_test( end, { test_init = test_init_x_y_color_mode, - min_api_version = 17 + min_api_version = 14 } ) @@ -979,7 +979,7 @@ test.register_message_test( }, { test_init = test_init_with_hue_sat_color_mode_set, - min_api_version = 17 + min_api_version = 14 } ) @@ -1065,7 +1065,7 @@ test.register_message_test( }, { test_init = test_init_with_hue_sat_color_mode_set, - min_api_version = 17 + min_api_version = 14 } ) @@ -1143,7 +1143,7 @@ test.register_coroutine_test( end, { test_init = test_init_no_hue_sat, - min_api_version = 17 + min_api_version = 15 } ) @@ -1195,7 +1195,7 @@ test.register_coroutine_test( end, { test_init = test_init_color_temp, - min_api_version = 17 + min_api_version = 15 } ) @@ -1222,7 +1222,7 @@ test.register_coroutine_test( end, { test_init = test_init_color_temp, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua index 788a00d76e..221405a62a 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_water_valve.lua @@ -69,7 +69,10 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) mock_device:expect_metadata_update({ profile = "water-valve-level" }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + end, + { + min_api_version = 15 + } ) test.register_message_test( @@ -93,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -118,7 +121,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -159,7 +162,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -179,8 +182,6 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.valve.valve.closed()) }, - }, - { { channel = "matter", direction = "receive", @@ -209,7 +210,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -231,7 +232,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua index 5b1b569b25..0b68428669 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_multi_switch_mcd.lua @@ -200,7 +200,7 @@ test.register_message_test( }, { test_init = test_init_mock_3switch, - min_api_version = 17 + min_api_version = 14 } ) @@ -227,7 +227,7 @@ test.register_message_test( }, { test_init = test_init_mock_2switch, - min_api_version = 17 + min_api_version = 14 } ) @@ -254,7 +254,7 @@ test.register_message_test( }, { test_init = test_init_mock_3switch_non_sequential, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua index a92c547ac7..eec293013b 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_stateless_step.lua @@ -128,7 +128,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -210,7 +210,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) 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 index a32ce8f8ff..8ed1a78031 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_third_reality_subdrivers.lua @@ -95,6 +95,9 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.doorControl.door.open()) } + }, + { + min_api_version = 15 } ) @@ -114,6 +117,9 @@ test.register_message_test( direction = "send", message = mock_device:generate_test_message("main", capabilities.doorControl.door.closed()) } + }, + { + min_api_version = 15 } ) @@ -143,6 +149,9 @@ test.register_message_test( clusters.OnOff.server.commands.On(mock_device, 1) } } + }, + { + min_api_version = 15 } ) @@ -170,6 +179,9 @@ test.register_message_test( clusters.OnOff.server.commands.Off(mock_device, 1) } } + }, + { + min_api_version = 15 } ) @@ -192,6 +204,9 @@ test.register_message_test( -- BatPercentRemaining is in units of 0.5%, so 200 = 100% message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) } + }, + { + min_api_version = 15 } ) @@ -212,6 +227,9 @@ test.register_message_test( -- 150 * 0.5 = 75% message = mock_device:generate_test_message("main", capabilities.battery.battery(75)) } + }, + { + min_api_version = 15 } ) @@ -224,7 +242,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({profile = "garage-door-battery"}) mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) end, - {min_api_version = 17} + {min_api_version = 15} ) test.register_coroutine_test( @@ -234,7 +252,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({profile = "garage-door-battery"}) mock_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) end, - {min_api_version = 17} + {min_api_version = 15} ) @@ -298,6 +316,7 @@ test.register_coroutine_test( end, { test_init = function() test.mock_device.add_test_device(mock_device_misprofiled) end, + min_api_version = 15 } ) @@ -525,7 +544,7 @@ test.register_coroutine_test( end, { test_init = test_init_mk1, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua index 0a97b12181..630896d25c 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier.lua @@ -432,7 +432,7 @@ test.register_coroutine_test( end, { test_init = test_init_ap_aqs, - min_api_version = 17 + min_api_version = 15 } ) @@ -448,7 +448,7 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs, - min_api_version = 17 + min_api_version = 15 } ) @@ -473,7 +473,7 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs_preconfigured, - min_api_version = 17 + min_api_version = 15 } ) @@ -530,7 +530,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -574,7 +574,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -612,7 +612,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -660,7 +660,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -721,7 +721,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -777,7 +777,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -799,7 +799,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -870,7 +870,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -888,7 +888,7 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs_preconfigured, - min_api_version = 17 + min_api_version = 15 } ) @@ -914,7 +914,7 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs_preconfigured, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua index 8bb8a6c920..4fed28f10f 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_api9.lua @@ -6,9 +6,6 @@ local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local SinglePrecisionFloat = require "st.matter.data_types.SinglePrecisionFloat" local t_utils = require "integration_test.utils" -local version = require "version" - -version.api = 9 -- include driver-side cluster definitions to test embedded clusters on lower api versions clusters.HepaFilterMonitoring = require "embedded_clusters.HepaFilterMonitoring" @@ -432,7 +429,8 @@ test.register_coroutine_test( end, { test_init = test_init_ap_aqs, - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -448,7 +446,8 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs, - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -473,7 +472,8 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs_preconfigured, - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -530,7 +530,8 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -574,7 +575,8 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -612,7 +614,8 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -660,7 +663,8 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -721,7 +725,8 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -778,7 +783,8 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -800,7 +806,8 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -872,7 +879,8 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -890,7 +898,8 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs_preconfigured, - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) @@ -916,7 +925,8 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs_preconfigured, - min_api_version = 17 + min_api_version = 9, + max_api_version = 10 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua index 6aefcb5045..fcc8740458 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_air_purifier_modular.lua @@ -317,7 +317,7 @@ test.register_coroutine_test( end, { test_init = test_init_basic, - min_api_version = 17 + min_api_version = 15 } ) @@ -394,7 +394,7 @@ test.register_coroutine_test( end, { test_init = test_init_ap_thermo_aqs_preconfigured, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua index 91d3303f78..8d8964acf0 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_fan.lua @@ -113,7 +113,7 @@ test.register_coroutine_test( end, { test_init = test_init, - min_api_version = 17 + min_api_version = 15 } ) @@ -128,7 +128,7 @@ test.register_coroutine_test( end, { test_init = test_init_generic, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua index 66c6e593ed..730a5db48a 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_heat_pump.lua @@ -148,7 +148,7 @@ test.register_coroutine_test( assert(component_to_endpoint_map["thermostatTwo"] == THERMOSTAT_TWO_EP, string.format("Thermostat Two Endpoint must be %d", THERMOSTAT_TWO_EP)) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -193,7 +193,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -238,7 +238,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -279,7 +279,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -320,7 +320,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -387,7 +387,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -475,7 +475,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -546,7 +546,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -613,7 +613,7 @@ test.register_message_test( }, { test_init = test_init_auto, - min_api_version = 17 + min_api_version = 15 } ) @@ -637,7 +637,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -662,7 +662,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -710,7 +710,7 @@ test.register_coroutine_test( test_init = function() test_init() end, - min_api_version = 17 + min_api_version = 15 } ) @@ -763,7 +763,7 @@ test.register_coroutine_test( test_init = function() test_init() end, - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua index 2f7085bff2..012fc0ab66 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac.lua @@ -296,7 +296,7 @@ test.register_coroutine_test( end, { test_init = test_init_configure, - min_api_version = 17 + min_api_version = 15 } ) @@ -317,7 +317,7 @@ test.register_coroutine_test( end, { test_init = test_init_nostate, - min_api_version = 17 + min_api_version = 15 } ) @@ -355,7 +355,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -411,7 +411,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -459,7 +459,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -495,7 +495,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -544,7 +544,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -584,7 +584,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua index aac1ecd4bc..fe15fdb026 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_room_ac_modular.lua @@ -314,7 +314,7 @@ test.register_coroutine_test( end, { test_init = test_init_basic, - min_api_version = 17 + min_api_version = 15 } ) @@ -342,7 +342,7 @@ test.register_coroutine_test( end, { test_init = test_init_no_state, - min_api_version = 17 + min_api_version = 15 } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua index 5352a65a9d..0206d949f5 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_battery.lua @@ -107,7 +107,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "thermostat-cooling-only-nostate" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -129,7 +129,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "thermostat-cooling-only-nostate-batteryLevel" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua index 377725fba0..efdeeec6fe 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_featuremap.lua @@ -228,7 +228,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ profile = "thermostat-humidity-fan-heating-only" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -252,7 +252,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -277,7 +277,7 @@ test.register_coroutine_test( mock_device_simple:expect_metadata_update({ profile = "thermostat-cooling-only-nostate" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -294,7 +294,7 @@ test.register_coroutine_test( mock_device_no_battery:expect_metadata_update({ profile = "thermostat-cooling-only-nostate-nobattery" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua index 4e7704feb1..ac72e56018 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_multiple_device_types.lua @@ -240,7 +240,7 @@ test.register_coroutine_test( get_subscribe_request(mock_device, new_cluster_subscribe_list)) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -252,7 +252,7 @@ test.register_coroutine_test( end, { test_init = test_init_disorder_endpoints, - min_api_version = 17 + min_api_version = 15 } ) @@ -286,7 +286,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua index 1d772fb2f1..ae5dcc10af 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits.lua @@ -150,7 +150,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -179,7 +179,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -237,7 +237,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -259,7 +259,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -281,7 +281,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -297,7 +297,7 @@ test.register_coroutine_test( assert(min_setpoint_deadband_checked == true, "min_setpoint_deadband_checked is True") end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -327,7 +327,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -357,7 +357,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -387,7 +387,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua index 3dd153d270..13702d015d 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermo_setpoint_limits_rpc.lua @@ -101,7 +101,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -126,7 +126,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -151,7 +151,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua index d1373c036d..c7ab7d646f 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat.lua @@ -169,7 +169,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -191,7 +191,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -213,7 +213,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -240,7 +240,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -267,7 +267,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -289,7 +289,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -311,7 +311,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -333,7 +333,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -355,7 +355,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -392,7 +392,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -441,7 +441,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -490,7 +490,7 @@ test.register_message_test( }, { test_init = test_init_auto, - min_api_version = 17 + min_api_version = 15 } ) @@ -530,7 +530,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -571,7 +571,7 @@ test.register_message_test( }, { test_init = test_init_auto, - min_api_version = 17 + min_api_version = 15 } ) @@ -623,7 +623,7 @@ test.register_message_test( }, { test_init = test_init_auto, - min_api_version = 17 + min_api_version = 15 } ) @@ -680,7 +680,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -716,7 +716,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -741,7 +741,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -766,7 +766,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -791,7 +791,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -832,7 +832,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -857,7 +857,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -878,7 +878,7 @@ test.register_coroutine_test("Battery percent reports should generate correct me test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -933,7 +933,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua index af44bf73f6..07e9e94604 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_composed_bridged.lua @@ -98,7 +98,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -121,7 +121,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -144,7 +144,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -173,7 +173,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -202,7 +202,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -225,7 +225,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -248,7 +248,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -271,7 +271,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -294,7 +294,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -329,7 +329,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -382,7 +382,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -424,7 +424,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -474,7 +474,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -512,7 +512,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -537,7 +537,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -562,7 +562,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -587,7 +587,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -628,7 +628,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -653,7 +653,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -674,7 +674,7 @@ test.register_coroutine_test("Battery percent reports should generate correct me test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -729,7 +729,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua index a678e85499..b1aca3242e 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_modular.lua @@ -179,7 +179,7 @@ test.register_coroutine_test( end, { test_init = test_init, - min_api_version = 17 + min_api_version = 15 } ) @@ -245,7 +245,10 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({mock_device_modular.id, subscribe_request}) end, - { test_init = test_init_modular_fingerprint } + { + test_init = test_init_modular_fingerprint, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -253,7 +256,10 @@ test.register_coroutine_test( -- simulate no actual change test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({})) end, - { test_init = function() test.mock_device.add_test_device(mock_device_modular) end } + { + test_init = function() test.mock_device.add_test_device(mock_device_modular) end, + min_api_version = 15 + } ) local function initialize_subscribe_request(mock_device, subscribed_attributes) @@ -318,7 +324,10 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({ profile = updated_device_profile })) test.socket.matter:__expect_send({mock_device_modular.id, subscribe_request}) end, - { test_init = test_init_modular_fingerprint } + { + test_init = test_init_modular_fingerprint, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -326,7 +335,10 @@ test.register_coroutine_test( -- simulate no actual change test.socket.device_lifecycle:__queue_receive(mock_device_modular:generate_info_changed({})) end, - { test_init = function() test.mock_device.add_test_device(mock_device_modular) end } + { + test_init = function() test.mock_device.add_test_device(mock_device_modular) end, + min_api_version = 15 + } ) -- run tests diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua index f7078703ac..6c05f671e5 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_thermostat_rpc5.lua @@ -137,7 +137,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua b/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua index b4336beeb4..7c279a68c0 100644 --- a/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua +++ b/drivers/SmartThings/matter-thermostat/src/test/test_matter_water_heater.lua @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -142,7 +142,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -204,7 +204,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -236,7 +236,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -261,7 +261,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -331,7 +331,7 @@ test.register_coroutine_test( test_init = function() test_init() end, - min_api_version = 17 + min_api_version = 15 } ) @@ -433,7 +433,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua index e39cb428c8..1c61a6c96a 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_closure.lua @@ -300,7 +300,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -323,7 +326,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -343,7 +349,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -363,7 +372,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.windowShade.windowShade.open()) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -383,7 +395,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -427,7 +442,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.windowShade.windowShade.closed()) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -442,7 +460,10 @@ test.register_coroutine_test( mock_device, 10, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_CLOSED ) }) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -457,7 +478,10 @@ test.register_coroutine_test( mock_device, 10, clusters.ClosureControl.types.TargetPositionEnum.MOVE_TO_FULLY_OPEN ) }) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -470,7 +494,10 @@ test.register_coroutine_test( mock_device.id, clusters.ClosureControl.server.commands.Stop(mock_device, 10) }) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -484,7 +511,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(math.floor(150 / 2.0 + 0.5))) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -499,7 +529,10 @@ test.register_coroutine_test( mock_device.id, clusters.ClosureDimension.server.commands.SetTarget(mock_device, 11, 75 * 100) }) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -514,7 +547,10 @@ test.register_coroutine_test( mock_device.id, clusters.ClosureDimension.server.commands.SetTarget(mock_device, 12, 40 * 100) }) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -534,7 +570,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(60)) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -554,7 +593,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("windowShade2", capabilities.windowShadeLevel.shadeLevel(25)) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -574,7 +616,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(0)) ) - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -594,7 +639,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("windowShade1", capabilities.windowShadeLevel.shadeLevel(100)) ) - end + end, + { + min_api_version = 15 + } ) -- --------------------------------------------------------------------------- @@ -622,7 +670,10 @@ test.register_coroutine_test( mock_door_device:generate_test_message("main", capabilities.doorControl.door.closing()) ) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 17 + } ) test.register_coroutine_test( @@ -646,7 +697,10 @@ test.register_coroutine_test( mock_door_device:generate_test_message("main", capabilities.doorControl.door.opening()) ) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 17 + } ) test.register_coroutine_test( @@ -667,9 +721,13 @@ test.register_coroutine_test( mock_door_device:generate_test_message("main", capabilities.doorControl.door.closed()) ) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 17 + } ) + test.register_coroutine_test( "doorControl open following OverallCurrentState FULLY_OPENED", function() update_profile_door() @@ -688,7 +746,10 @@ test.register_coroutine_test( mock_door_device:generate_test_message("main", capabilities.doorControl.door.open()) ) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 17 + } ) test.register_coroutine_test( @@ -710,7 +771,10 @@ test.register_coroutine_test( mock_door_device:generate_test_message("main", capabilities.doorControl.door.open()) ) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 17 + } ) test.register_coroutine_test( @@ -726,7 +790,10 @@ test.register_coroutine_test( ) }) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 15 + } ) test.register_coroutine_test( @@ -742,9 +809,11 @@ test.register_coroutine_test( ) }) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 15 + } ) - test.register_coroutine_test( "ClosureDimension CurrentState on endpoint 11 emits level on door1 for door device", function() update_profile_door() @@ -763,7 +832,10 @@ test.register_coroutine_test( mock_door_device:generate_test_message("door1", capabilities.level.level(75)) ) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 17 + } ) test.register_coroutine_test( @@ -784,7 +856,10 @@ test.register_coroutine_test( mock_door_device:generate_test_message("door2", capabilities.level.level(30)) ) end, - {test_init = test_init_door} + { + test_init = test_init_door, + min_api_version = 17 + } ) test.run_registered_tests() diff --git a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua index 8376e3a6d4..7199e3704d 100644 --- a/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua +++ b/drivers/SmartThings/matter-window-covering/src/test/test_matter_window_covering.lua @@ -520,7 +520,10 @@ test.register_coroutine_test( "main", capabilities.windowShade.windowShade.partially_open() ) ) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( diff --git a/drivers/SmartThings/virtual-switch/src/test/test_virtual_switch.lua b/drivers/SmartThings/virtual-switch/src/test/test_virtual_switch.lua index 5c284b1e6b..1feb54fb0d 100644 --- a/drivers/SmartThings/virtual-switch/src/test/test_virtual_switch.lua +++ b/drivers/SmartThings/virtual-switch/src/test/test_virtual_switch.lua @@ -46,7 +46,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -65,7 +65,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -104,7 +104,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.switch.switch.on())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua b/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua index 606b716dff..b9ae4cab1a 100644 --- a/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua +++ b/drivers/SmartThings/zigbee-air-quality-detector/src/test/test_MultiIR_air_quality_detector.lua @@ -73,7 +73,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, read_AQI_messge}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -137,7 +137,7 @@ test.register_coroutine_test( capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "good"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_coroutine_test( capabilities.fineDustHealthConcern.fineDustHealthConcern.good())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ test.register_coroutine_test( capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern.good())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_coroutine_test( capabilities.dustHealthConcern.dustHealthConcern.good())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -215,7 +215,7 @@ test.register_coroutine_test( capabilities.formaldehydeMeasurement.formaldehydeLevel({value = 1000.0, unit = "mg/m^3"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -235,7 +235,7 @@ test.register_coroutine_test( capabilities.tvocHealthConcern.tvocHealthConcern({value = "unhealthy"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_coroutine_test( capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "good"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -271,7 +271,7 @@ test.register_coroutine_test( capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "moderate"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -289,7 +289,7 @@ test.register_coroutine_test( capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "slightlyUnhealthy"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -307,7 +307,7 @@ test.register_coroutine_test( capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "unhealthy"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -325,7 +325,7 @@ test.register_coroutine_test( capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "veryUnhealthy"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -343,7 +343,7 @@ test.register_coroutine_test( capabilities.airQualityHealthConcern.airQualityHealthConcern({value = "hazardous"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -363,7 +363,7 @@ test.register_coroutine_test( capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "moderate"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -383,7 +383,7 @@ test.register_coroutine_test( capabilities.carbonDioxideHealthConcern.carbonDioxideHealthConcern({value = "unhealthy"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -403,7 +403,7 @@ test.register_coroutine_test( capabilities.fineDustHealthConcern.fineDustHealthConcern({value = "moderate"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -423,7 +423,7 @@ test.register_coroutine_test( capabilities.fineDustHealthConcern.fineDustHealthConcern({value = "unhealthy"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -443,7 +443,7 @@ test.register_coroutine_test( capabilities.veryFineDustHealthConcern.veryFineDustHealthConcern({value = "unhealthy"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -463,7 +463,7 @@ test.register_coroutine_test( capabilities.dustHealthConcern.dustHealthConcern({value = "unhealthy"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -483,7 +483,7 @@ test.register_coroutine_test( capabilities.tvocHealthConcern.tvocHealthConcern({value = "good"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua index 2b02b5ec73..08eb79e2a9 100644 --- a/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua +++ b/drivers/SmartThings/zigbee-bed/src/test/test_shus_mattress.lua @@ -86,7 +86,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, read_0x0011_messge}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, read_0x0011_messge}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftback.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -185,7 +185,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftwaist.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_coroutine_test( custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -239,7 +239,7 @@ test.register_coroutine_test( custom_capabilities.left_control.lefthip.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -257,7 +257,7 @@ test.register_coroutine_test( custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_coroutine_test( custom_capabilities.right_control.rightback.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -293,7 +293,7 @@ test.register_coroutine_test( custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -311,7 +311,7 @@ test.register_coroutine_test( custom_capabilities.right_control.rightwaist.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -329,7 +329,7 @@ test.register_coroutine_test( custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -347,7 +347,7 @@ test.register_coroutine_test( custom_capabilities.right_control.righthip.idle({ visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -365,7 +365,7 @@ test.register_coroutine_test( custom_capabilities.mattressHardness.leftBackHardness(1))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -383,7 +383,7 @@ test.register_coroutine_test( custom_capabilities.mattressHardness.leftWaistHardness(1))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -401,7 +401,7 @@ test.register_coroutine_test( custom_capabilities.mattressHardness.leftHipHardness(1))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -419,7 +419,7 @@ test.register_coroutine_test( custom_capabilities.mattressHardness.rightBackHardness(1))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -437,7 +437,7 @@ test.register_coroutine_test( custom_capabilities.mattressHardness.rightWaistHardness(1))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -455,7 +455,7 @@ test.register_coroutine_test( custom_capabilities.mattressHardness.rightHipHardness(1))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -473,7 +473,7 @@ test.register_coroutine_test( custom_capabilities.yoga.state.both())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -491,7 +491,7 @@ test.register_coroutine_test( custom_capabilities.yoga.state.right())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -509,7 +509,7 @@ test.register_coroutine_test( custom_capabilities.yoga.state.left())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -527,7 +527,7 @@ test.register_coroutine_test( custom_capabilities.yoga.state.stop())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -545,7 +545,7 @@ test.register_coroutine_test( custom_capabilities.ai_mode.left.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -563,7 +563,7 @@ test.register_coroutine_test( custom_capabilities.ai_mode.left.on())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -581,7 +581,7 @@ test.register_coroutine_test( custom_capabilities.ai_mode.right.on())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -599,7 +599,7 @@ test.register_coroutine_test( custom_capabilities.ai_mode.right.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -617,7 +617,7 @@ test.register_coroutine_test( custom_capabilities.auto_inflation.inflationState.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -635,7 +635,7 @@ test.register_coroutine_test( custom_capabilities.auto_inflation.inflationState.on())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -653,7 +653,7 @@ test.register_coroutine_test( custom_capabilities.strong_exp_mode.expState.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -671,7 +671,7 @@ test.register_coroutine_test( custom_capabilities.strong_exp_mode.expState.on())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -689,7 +689,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -706,7 +706,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -723,7 +723,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -740,7 +740,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -757,7 +757,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -774,7 +774,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -791,7 +791,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -808,7 +808,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -827,7 +827,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftback.soft())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -846,7 +846,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftwaist.soft())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -865,7 +865,7 @@ test.register_coroutine_test( custom_capabilities.left_control.lefthip.soft())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -884,7 +884,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftback.hard())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -903,7 +903,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftwaist.hard())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -922,7 +922,7 @@ test.register_coroutine_test( custom_capabilities.left_control.lefthip.hard())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -941,7 +941,7 @@ test.register_coroutine_test( custom_capabilities.right_control.rightback.soft())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -960,7 +960,7 @@ test.register_coroutine_test( custom_capabilities.right_control.rightwaist.soft())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -979,7 +979,7 @@ test.register_coroutine_test( custom_capabilities.right_control.righthip.soft())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -998,7 +998,7 @@ test.register_coroutine_test( custom_capabilities.right_control.rightback.hard())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1017,7 +1017,7 @@ test.register_coroutine_test( custom_capabilities.right_control.rightwaist.hard())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1036,7 +1036,7 @@ test.register_coroutine_test( custom_capabilities.right_control.righthip.hard())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1053,7 +1053,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1070,7 +1070,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1087,7 +1087,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1112,7 +1112,7 @@ test.register_coroutine_test( custom_capabilities.left_control.leftback("idle", { visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua index 342a62a5c7..288d531148 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_SLED_button.lua @@ -50,7 +50,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -74,7 +74,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -89,7 +89,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua index 808eb0a764..d44d73da7a 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aduro_button.lua @@ -93,7 +93,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -179,7 +179,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua index 49883cd275..da3fe73c48 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_aqara_button.lua @@ -99,7 +99,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_coroutine_test( mock_device_e1:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -166,7 +166,7 @@ test.register_coroutine_test( mock_device_h1_double_rocker:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_coroutine_test( capabilities.button.button.pushed({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( capabilities.button.button.double({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_coroutine_test( capabilities.button.button.held({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -248,7 +248,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) test.register_message_test( @@ -266,7 +266,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) test.register_message_test( @@ -284,7 +284,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -328,7 +328,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -350,7 +350,7 @@ test.register_coroutine_test( capabilities.batteryLevel.quantity(1))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua index be95f76cce..c08a42e4c0 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_centralite_button.lua @@ -95,7 +95,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -191,7 +191,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua b/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua index db9bad6ea9..17170106a7 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_dimming_remote.lua @@ -55,7 +55,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua index 773021ac20..61e30bfaee 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ewelink_button.lua @@ -64,7 +64,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -79,7 +79,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -111,7 +111,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -151,7 +151,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua index 07652b0e0e..b012f30a8d 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ezviz_button.lua @@ -55,7 +55,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -137,7 +137,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device_ezviz_button.id, ZoneStatusAttribute:read(mock_device_ezviz_button) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua index 40d74e503c..6dc9b979c1 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_frient_button.lua @@ -88,7 +88,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_message_test("Refresh should read all necessary attributes", { }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ test.register_coroutine_test("panicAlarm should be triggered and cleared", funct end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -184,7 +184,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -226,7 +226,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ function() test.socket.zigbee:__expect_send({mock_device.id, buttonDelay_msg}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -286,7 +286,7 @@ test.register_coroutine_test(" Configuration and Switching to button-profile-pan --test.socket.capability:__expect_send({mock_device.id, capabilities.panicAlarm.panicAlarm.clear({state_change = true})}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -302,7 +302,7 @@ test.register_coroutine_test("Switching from button-profile-panic-frient to butt test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic,BasicInput.ID,0x8000,DEVELCO_MANUFACTURER_CODE,data_types.Uint16,0xFFFF)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -324,7 +324,7 @@ test.register_coroutine_test("New preferences after switching the profile should test.socket.zigbee:__expect_send({mock_device_panic.id, cluster_base.write_manufacturer_specific_attribute(mock_device_panic, IASZone.ID,0x8005,DEVELCO_MANUFACTURER_CODE,data_types.Enum8, 1)}) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua index 7e9448ed7e..7142b8ad8c 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_heiman_button.lua @@ -92,7 +92,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -174,7 +174,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ test.register_coroutine_test( mock_device_hs6ssb:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -279,7 +279,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ test.register_coroutine_test( mock_device_hs6ssb:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -361,7 +361,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -409,7 +409,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ikea_on_off.lua b/drivers/SmartThings/zigbee-button/src/test/test_ikea_on_off.lua index 4a772c5ab0..d3d1d75c67 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ikea_on_off.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ikea_on_off.lua @@ -75,7 +75,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -143,7 +143,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_add_hub_to_group(0xB9F2) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -304,7 +304,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ikea_open_close.lua b/drivers/SmartThings/zigbee-button/src/test/test_ikea_open_close.lua index d350567294..56a8d3c382 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ikea_open_close.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ikea_open_close.lua @@ -55,7 +55,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_add_hub_to_group(0xB9F2) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, Groups.commands.AddGroup(mock_device, 0x0000) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_ikea_remote_control.lua b/drivers/SmartThings/zigbee-button/src/test/test_ikea_remote_control.lua index 1fc0e9d8e6..92360b9b5d 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_ikea_remote_control.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_ikea_remote_control.lua @@ -109,7 +109,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -154,7 +154,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -179,7 +179,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_add_hub_to_group(0xB9F2) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -286,7 +286,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -305,7 +305,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua index 81e68a52da..14284ca482 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_iris_button.lua @@ -47,7 +47,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -63,7 +63,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -240,7 +240,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -261,7 +261,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_linxura_aura_smart_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_linxura_aura_smart_button.lua index a70d3b52a2..b2eecc400c 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_linxura_aura_smart_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_linxura_aura_smart_button.lua @@ -57,7 +57,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_linxura_smart_controller_4x_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_linxura_smart_controller_4x_button.lua index 35ad7aae08..406bcd018f 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_linxura_smart_controller_4x_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_linxura_smart_controller_4x_button.lua @@ -57,7 +57,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_push_only_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_push_only_button.lua index 2f6deceb70..527c15ade7 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_push_only_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_push_only_button.lua @@ -48,7 +48,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) test.register_message_test( @@ -61,7 +61,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -80,7 +80,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -164,7 +164,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_robb_4x_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_robb_4x_button.lua index bbe5cbaf86..0f014d8b41 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_robb_4x_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_robb_4x_button.lua @@ -81,7 +81,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -212,7 +212,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -326,7 +326,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -368,7 +368,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -386,7 +386,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_robb_8x_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_robb_8x_button.lua index 8c291f8483..c073b7e051 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_robb_8x_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_robb_8x_button.lua @@ -126,7 +126,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -284,7 +284,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -329,7 +329,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -444,7 +444,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -486,7 +486,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -504,7 +504,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua index 72bcd2009c..be5af30cbc 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_samjin_button.lua @@ -90,7 +90,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -140,7 +140,7 @@ test.register_coroutine_test( -- }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua index d5eed5d55f..2a3f825ac9 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_shinasystem_button.lua @@ -85,7 +85,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -239,7 +239,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -287,7 +287,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_1_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_1_button.lua index 2793a6de1b..f5f91aec73 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_1_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_1_button.lua @@ -65,7 +65,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -140,7 +140,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_add_hub_to_group(0xB9F2) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_4_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_4_button.lua index 9e209541ff..bd4d767f2f 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_4_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_somfy_situo_4_button.lua @@ -94,7 +94,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_add_hub_to_group(0xB9F2) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -235,7 +235,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua index 22f76bc383..4e02098b0b 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_sonoff_button.lua @@ -161,7 +161,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -180,7 +180,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -218,7 +218,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -237,7 +237,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -256,7 +256,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -271,7 +271,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua index f4ee2e0aed..7c9472df39 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_thirdreality_button.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -65,7 +65,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -82,7 +82,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.double({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.held({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua index c76d080a01..f1f4413429 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_vimar_button.lua @@ -77,7 +77,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -165,7 +165,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua index 93c6d26139..d4fcdd87f1 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_wallhero_button.lua @@ -306,7 +306,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -347,7 +347,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua index db4f6b9a43..a854809234 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_button.lua @@ -39,7 +39,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -58,7 +58,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -77,7 +77,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -91,7 +91,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -296,7 +296,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -373,7 +373,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_ecosmart_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_ecosmart_button.lua index 1b3078f637..cc75674c85 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_zigbee_ecosmart_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_zigbee_ecosmart_button.lua @@ -50,7 +50,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -212,7 +212,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -261,7 +261,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-button/src/test/test_zunzunbee_8_button.lua b/drivers/SmartThings/zigbee-button/src/test/test_zunzunbee_8_button.lua index 13479d4e8b..5e288c6344 100644 --- a/drivers/SmartThings/zigbee-button/src/test/test_zunzunbee_8_button.lua +++ b/drivers/SmartThings/zigbee-button/src/test/test_zunzunbee_8_button.lua @@ -81,7 +81,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_climax_technology_carbon_monoxide.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_climax_technology_carbon_monoxide.lua index b0df2ffaa4..fb92f80e90 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_climax_technology_carbon_monoxide.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_climax_technology_carbon_monoxide.lua @@ -42,7 +42,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_frient_co_smoke_temperature_battery.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_frient_co_smoke_temperature_battery.lua index 16625fdf92..36bad5633a 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_frient_co_smoke_temperature_battery.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_frient_co_smoke_temperature_battery.lua @@ -447,7 +447,10 @@ test.register_coroutine_test( ) }) end -) +, + { + min_api_version = 17 + }) test.register_coroutine_test( "Alarm off command should send StartWarning stop", @@ -474,7 +477,10 @@ test.register_coroutine_test( ) }) end -) +, + { + min_api_version = 17 + }) test.register_coroutine_test( "Default response to StartWarning should emit alarm events", @@ -500,7 +506,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 17 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_zigbee_carbon_monoxide.lua b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_zigbee_carbon_monoxide.lua index 1b67dc8ee6..ab593688c3 100644 --- a/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_zigbee_carbon_monoxide.lua +++ b/drivers/SmartThings/zigbee-carbon-monoxide-detector/src/test/test_zigbee_carbon_monoxide.lua @@ -44,7 +44,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -71,7 +71,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_message_test( }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -126,7 +126,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua index 8f6c5ecfdd..014e2b8ed1 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua @@ -66,7 +66,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -90,7 +90,7 @@ test.register_coroutine_test( capabilities.batteryLevel.battery("normal"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_coroutine_test( capabilities.batteryLevel.battery("normal"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -126,7 +126,7 @@ test.register_coroutine_test( capabilities.batteryLevel.battery("critical"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( capabilities.batteryLevel.battery("normal"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ test.register_coroutine_test( capabilities.batteryLevel.battery("warning"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -180,7 +180,7 @@ test.register_coroutine_test( capabilities.batteryLevel.battery("critical"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ test.register_coroutine_test( capabilities.contactSensor.contact.closed())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_coroutine_test( capabilities.contactSensor.contact.open())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_aurora_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_aurora_contact_sensor.lua index a2bcfdf612..395b85063f 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_aurora_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_aurora_contact_sensor.lua @@ -44,7 +44,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_centralite_multi_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_centralite_multi_sensor.lua index b13d48fc2e..09f0108325 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_centralite_multi_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_centralite_multi_sensor.lua @@ -108,7 +108,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -134,7 +134,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.inactive()) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -282,7 +282,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -307,7 +307,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_contact_temperature_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_contact_temperature_sensor.lua index 62e0eee71b..b0ce27f524 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_contact_temperature_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_contact_temperature_sensor.lua @@ -46,7 +46,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua b/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua index e092666737..1341f0a518 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_ecolink_contact.lua @@ -47,7 +47,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_ewelink_heiman_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_ewelink_heiman_sensor.lua index f3b94929b6..05d030b0d3 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_ewelink_heiman_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_ewelink_heiman_sensor.lua @@ -45,7 +45,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor.lua index 8be3dd430a..e766f0a4d5 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -135,7 +135,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -154,7 +154,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -207,7 +207,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -226,7 +226,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -245,7 +245,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -264,7 +264,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -283,7 +283,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua index f6a2e0ebd9..3911b323c1 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_2_pro.lua @@ -56,7 +56,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -178,7 +178,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -224,7 +224,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -322,7 +322,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -341,7 +341,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -360,7 +360,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -379,7 +379,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -398,7 +398,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -426,7 +426,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_pro.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_pro.lua index 66881f9f0b..e21f06f588 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_pro.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_contact_sensor_pro.lua @@ -56,7 +56,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -178,7 +178,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -224,7 +224,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -322,7 +322,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -346,7 +346,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -370,7 +370,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -394,7 +394,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -418,7 +418,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -446,7 +446,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua index 7829e32736..d386758433 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua @@ -300,7 +300,7 @@ test.register_coroutine_test( mock_device_contact:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -328,7 +328,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -347,7 +347,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -366,7 +366,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -407,7 +407,7 @@ function() }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -431,7 +431,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -455,7 +455,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -477,7 +477,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -505,7 +505,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -533,7 +533,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_orvibo_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_orvibo_contact_sensor.lua index 6ebb00d45e..9e4bd2aa0a 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_orvibo_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_orvibo_contact_sensor.lua @@ -44,7 +44,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_samjin_multi_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_samjin_multi_sensor.lua index d4082723c7..7d854c2f6e 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_samjin_multi_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_samjin_multi_sensor.lua @@ -95,7 +95,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -180,7 +180,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -222,7 +222,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_sengled_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_sengled_contact_sensor.lua index fc48ab8eaa..f3e92f2c9a 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_sengled_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_sengled_contact_sensor.lua @@ -44,7 +44,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua b/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua index 4713e6eb41..701aba257b 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_smartsense_multi.lua @@ -116,7 +116,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.active())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -134,7 +134,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -206,7 +206,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -224,7 +224,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -242,7 +242,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -260,7 +260,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -278,7 +278,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(97))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -296,7 +296,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -314,7 +314,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(60))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -332,7 +332,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -350,7 +350,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -368,7 +368,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -383,7 +383,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({1050, 3, 9})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -398,7 +398,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({-1050, -3, -9})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -413,7 +413,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({10, 1020, 7})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -428,7 +428,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({-10, -1020, -7})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -443,7 +443,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({116, 4, 1003})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -458,7 +458,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({-116, -4, -1003})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -491,7 +491,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -510,7 +510,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -529,7 +529,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -548,7 +548,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -567,7 +567,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_smartthings_multi_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_smartthings_multi_sensor.lua index da87c0aee2..770b8e6718 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_smartthings_multi_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_smartthings_multi_sensor.lua @@ -89,7 +89,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -134,7 +134,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.inactive()) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -151,7 +151,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.active()) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 100, -200})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -206,7 +206,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -236,7 +236,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -255,7 +255,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -274,7 +274,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -325,7 +325,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -390,7 +390,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -434,7 +434,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_third_reality_contact.lua b/drivers/SmartThings/zigbee-contact/src/test/test_third_reality_contact.lua index c03ab49dde..0273886a4c 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_third_reality_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_third_reality_contact.lua @@ -44,7 +44,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_thirdreality_multi_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_thirdreality_multi_sensor.lua index 92f7274365..924205c3ff 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_thirdreality_multi_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_thirdreality_multi_sensor.lua @@ -54,7 +54,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.inactive()) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -71,7 +71,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.active()) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -90,7 +90,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({200, 100, 300})) ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua index f27743d4ee..1064145d99 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact.lua @@ -46,7 +46,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -185,7 +185,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -344,7 +344,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_battery.lua b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_battery.lua index d908aedba6..c7bce911b2 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_battery.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_battery.lua @@ -88,7 +88,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -128,7 +128,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_tyco.lua b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_tyco.lua index 6b482996de..625b8f0d6b 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_tyco.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_zigbee_contact_tyco.lua @@ -56,7 +56,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -105,7 +105,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua b/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua index b305bb8a9d..fbee4cf910 100644 --- a/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua +++ b/drivers/SmartThings/zigbee-fan/src/test/test_fan_light.lua @@ -50,7 +50,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -71,7 +71,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -92,7 +92,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -223,7 +223,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -263,7 +263,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -287,7 +287,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -311,7 +311,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -335,7 +335,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -365,7 +365,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -395,7 +395,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -425,7 +425,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -455,7 +455,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -485,7 +485,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -509,7 +509,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -533,7 +533,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_aqara_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_aqara_sensor.lua index 762c1f4431..87f8c0e85c 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_aqara_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_aqara_sensor.lua @@ -96,7 +96,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -165,7 +165,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -196,7 +196,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -241,7 +241,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -263,7 +263,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -285,7 +285,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -307,7 +307,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_centralite_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_centralite_sensor.lua index b35c590b91..6fdf421c04 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_centralite_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_centralite_sensor.lua @@ -95,7 +95,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -231,7 +231,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_ewelink_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_ewelink_sensor.lua index aefbd3c350..c8d5578e11 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_ewelink_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_ewelink_sensor.lua @@ -66,7 +66,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua index 06e836a007..eb022d7ebf 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_air_quality_sensor.lua @@ -85,7 +85,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -104,7 +104,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -262,7 +262,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -298,7 +298,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -328,7 +328,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -345,7 +345,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_sensor.lua index 81bc408f32..dfca8c6be1 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_frient_sensor.lua @@ -67,7 +67,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -189,7 +189,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -268,7 +268,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_heiman_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_heiman_sensor.lua index c75326503d..41a9d5ba22 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_heiman_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_heiman_sensor.lua @@ -66,7 +66,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_battery_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_battery_sensor.lua index 3d9e24e2eb..d23abc3777 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_battery_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_battery_sensor.lua @@ -48,7 +48,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -70,7 +70,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -92,7 +92,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_plaid_systems.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_plaid_systems.lua index c8aa3a6191..69a154e13e 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_plaid_systems.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_plaid_systems.lua @@ -51,7 +51,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -151,7 +151,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -210,7 +210,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -287,7 +287,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature.lua index e821dd3d56..dc3e6052c5 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature.lua @@ -48,7 +48,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -78,7 +78,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -137,7 +137,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -171,7 +171,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 79 }))) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_battery.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_battery.lua index b03f27d59d..6ffd1b5f28 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_battery.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_battery.lua @@ -60,7 +60,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -90,7 +90,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -191,7 +191,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.relativeHumidityMeasurement.humidity({ value = 79 }))) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_sensor.lua b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_sensor.lua index f01b9534bb..06a18c6063 100644 --- a/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_sensor.lua +++ b/drivers/SmartThings/zigbee-humidity-sensor/src/test/test_humidity_temperature_sensor.lua @@ -55,7 +55,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -85,7 +85,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor.lua index fb97045c03..f8c959141d 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor.lua @@ -46,7 +46,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -106,7 +106,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor_aqara.lua b/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor_aqara.lua index 32ee9eae11..8c519b17ca 100644 --- a/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor_aqara.lua +++ b/drivers/SmartThings/zigbee-illuminance-sensor/src/test/test_illuminance_sensor_aqara.lua @@ -59,7 +59,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_coroutine_test( MFG_CODE, data_types.Uint16, frequency) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_coroutine_test( detectionFrequency.detectionFrequency(FREQUENCY_DEFAULT_VALUE, { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_bad_battery_reporter.lua b/drivers/SmartThings/zigbee-lock/src/test/test_bad_battery_reporter.lua index 61fb28ce8d..6dfd87d155 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_bad_battery_reporter.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_bad_battery_reporter.lua @@ -46,7 +46,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ test.register_message_test( }, { test_init = test_init_yrd, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua index 4bde45ec1a..bf358af78c 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_c2o_lock.lua @@ -41,7 +41,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -75,7 +75,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -88,7 +88,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.LockState:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -154,7 +154,7 @@ test.register_coroutine_test( test.socket.zigbee:__queue_receive({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:build_test_attr_report(mock_device, 8)}) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_generic_fingerprint_profile_update.lua b/drivers/SmartThings/zigbee-lock/src/test/test_generic_fingerprint_profile_update.lua index e6d6a22b78..bd15eb3dd8 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_generic_fingerprint_profile_update.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_generic_fingerprint_profile_update.lua @@ -46,7 +46,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, DoorLock.attributes.NumberOfPINUsersSupported:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua index c3b316f486..accfb345ac 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_legacy.lua @@ -81,7 +81,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, Alarm.attributes.AlarmCount:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -196,7 +196,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -215,7 +215,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -273,7 +273,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -284,7 +284,7 @@ test.register_coroutine_test( expect_reload_all_codes_messages() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -303,7 +303,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -345,7 +345,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -395,7 +395,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -453,7 +453,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -471,7 +471,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -545,7 +545,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -580,7 +580,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -615,7 +615,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -654,7 +654,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -689,7 +689,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -746,7 +746,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -777,7 +777,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -843,7 +843,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -862,7 +862,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -881,7 +881,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -909,7 +909,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -939,7 +939,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "Code 1"}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua index 9ce880efe6..4d4a370168 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_lock_v10.lua @@ -81,7 +81,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, Alarm.attributes.AlarmCount:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -196,7 +196,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -215,7 +215,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -273,7 +273,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -284,7 +284,7 @@ test.register_coroutine_test( expect_reload_all_codes_messages() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -303,7 +303,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -345,7 +345,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -395,7 +395,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -453,7 +453,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -471,7 +471,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -545,7 +545,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -580,7 +580,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -615,7 +615,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -654,7 +654,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -689,7 +689,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -746,7 +746,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -777,7 +777,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -843,7 +843,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_fingerprint-lock.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_fingerprint-lock.lua index b0f4d1ffcf..560e0b3768 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_fingerprint-lock.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_fingerprint-lock.lua @@ -48,7 +48,7 @@ test.register_message_test( }, { test_init = test_init, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua index 95d496e786..82e487439b 100644 --- a/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua +++ b/drivers/SmartThings/zigbee-lock/src/test/test_zigbee_yale_legacy.lua @@ -47,7 +47,7 @@ test.register_coroutine_test( expect_reload_all_codes_messages() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -89,7 +89,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "test"}), { visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -189,7 +189,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["0"] = "test"}), { visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -218,7 +218,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -276,7 +276,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -321,7 +321,7 @@ test.register_coroutine_test( capabilities.lockCodes.codeChanged("1 failed", { state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -339,7 +339,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "foo"}), { visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -382,7 +382,7 @@ test.register_coroutine_test( capabilities.lockCodes.codeChanged("1 is not set", { state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -412,7 +412,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({["1"] = "initialName"}), { visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -442,7 +442,7 @@ test.register_coroutine_test( capabilities.lockCodes.lockCodes(json.encode({}), { visibility = { displayed = false }}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_all_capabilities_zigbee_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_all_capabilities_zigbee_motion.lua index 72fbeb168b..07cb3a9d99 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_all_capabilities_zigbee_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_all_capabilities_zigbee_motion.lua @@ -48,7 +48,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -75,7 +75,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -156,7 +156,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -182,7 +182,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -329,7 +329,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -416,7 +416,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -437,7 +437,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -457,7 +457,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_high_precision.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_high_precision.lua index 5e961e37ec..3b87239092 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_high_precision.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_high_precision.lua @@ -73,7 +73,7 @@ test.register_coroutine_test( capabilities.battery.battery(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_coroutine_test( capabilities.motionSensor.motion.inactive())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_coroutine_test( detectionFrequency.detectionFrequency(value, {visibility = {displayed = false}}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_coroutine_test( sensitivityAdjustment.sensitivityAdjustment.High())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -222,7 +222,7 @@ test.register_coroutine_test( sensitivityAdjustment.sensitivityAdjustment.Medium())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -239,7 +239,7 @@ test.register_coroutine_test( sensitivityAdjustment.sensitivityAdjustment.Low())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -277,7 +277,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_motion_illuminance.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_motion_illuminance.lua index a75033bef8..979c64d45b 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_motion_illuminance.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aqara_motion_illuminance.lua @@ -88,7 +88,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -111,7 +111,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_coroutine_test( capabilities.motionSensor.motion.inactive())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -158,7 +158,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_coroutine_test( capabilities.motionSensor.motion.inactive())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( detectionFrequency.detectionFrequency(PREF_FREQUENCY_VALUE_DEFAULT, {visibility = {displayed = false}}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aurora_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aurora_motion.lua index 4ae03f0354..f209f4f18f 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aurora_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_aurora_motion.lua @@ -56,7 +56,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -83,7 +83,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -137,7 +137,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_battery_voltage_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_battery_voltage_motion.lua index 1bb1e359bb..99c2da077d 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_battery_voltage_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_battery_voltage_motion.lua @@ -52,7 +52,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_centralite_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_centralite_motion.lua index fd1222b0cc..94c6779b44 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_centralite_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_centralite_motion.lua @@ -71,7 +71,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_compacta_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_compacta_motion.lua index ec85954568..03afa1c6e9 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_compacta_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_compacta_motion.lua @@ -77,7 +77,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua index a7b3828958..42a30de572 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor.lua @@ -71,7 +71,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, OccupancySensing.attributes.Occupancy:read(mock_device):to_endpoint(OCCUPANCY_ENDPOINT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -214,7 +214,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -233,7 +233,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -252,7 +252,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -284,7 +284,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua index 4dc67338d5..b84bfc6b6e 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor2_pet.lua @@ -79,7 +79,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -147,7 +147,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -359,7 +359,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -402,7 +402,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua index fe633e089a..f3e39499e0 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_frient_motion_sensor_pro.lua @@ -82,7 +82,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -121,7 +121,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -190,7 +190,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -206,7 +206,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, IlluminanceMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(ILLUMINANCE_ENDPOINT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -367,7 +367,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -410,7 +410,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -430,7 +430,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -450,7 +450,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_gator_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_gator_motion.lua index ad129ddc2b..343d4921c1 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_gator_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_gator_motion.lua @@ -46,7 +46,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -65,7 +65,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.presenceSensor.presence.not_present())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(0))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(10))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -182,7 +182,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_ikea_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_ikea_motion.lua index f922439c22..bd1f959bc7 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_ikea_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_ikea_motion.lua @@ -60,7 +60,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_add_hub_to_group(0xB9F2) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -211,7 +211,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -237,7 +237,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -265,7 +265,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_samjin_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_samjin_sensor.lua index 95c24f5f8e..2e098b3e69 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_samjin_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_samjin_sensor.lua @@ -95,7 +95,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_sengled_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_sengled_motion.lua index 4ca87d6358..9f4c74dc30 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_sengled_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_sengled_motion.lua @@ -46,7 +46,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 0xFFFF, 0x0000, 0) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartsense_motion_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartsense_motion_sensor.lua index b8b8fc4c62..c6e4d022e6 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartsense_motion_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartsense_motion_sensor.lua @@ -69,7 +69,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.rssi({ value = -50, unit = "dBm" }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.rssi({ value = -50, unit = "dBm" }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -103,7 +103,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.rssi({ value = -50, unit = "dBm" }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.rssi({ value = -50, unit = "dBm" }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.rssi({ value = -50, unit = "dBm" }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.rssi({ value = -50, unit = "dBm" }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.rssi({ value = -50, unit = "dBm" }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -182,7 +182,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.signalStrength.rssi({value = -100, unit = 'dBm'}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartthings_motion.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartthings_motion.lua index 83c8f3c36d..0f6c4039e2 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartthings_motion.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_smartthings_motion.lua @@ -55,7 +55,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_thirdreality_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_thirdreality_sensor.lua index cae6559187..45328961e9 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_thirdreality_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_thirdreality_sensor.lua @@ -70,7 +70,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -122,7 +122,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_iris.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_iris.lua index 7808c5184a..5794245167 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_iris.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_iris.lua @@ -56,7 +56,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -143,7 +143,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_nyce.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_nyce.lua index 9dca781210..01e7785287 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_nyce.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_nyce.lua @@ -46,7 +46,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -65,7 +65,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -94,7 +94,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_orvibo.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_orvibo.lua index c9172d8b8f..82f1b9509b 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_orvibo.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_motion_orvibo.lua @@ -46,7 +46,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -65,7 +65,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -103,7 +103,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -130,7 +130,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_plugin_motion_sensor.lua b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_plugin_motion_sensor.lua index 3bc0f85f0c..e93a03b861 100644 --- a/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_plugin_motion_sensor.lua +++ b/drivers/SmartThings/zigbee-motion-sensor/src/test/test_zigbee_plugin_motion_sensor.lua @@ -49,7 +49,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua index 6a312c1906..d752ccd553 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_battery_consumption_report.lua @@ -155,7 +155,10 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -) +, + { + min_api_version = 15 + }) test.register_message_test( "Refresh should read all necessary attributes", diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua index 2679329fd3..ae8ad437d9 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_consumption_report.lua @@ -140,7 +140,10 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -) +, + { + min_api_version = 15 + }) test.register_message_test( "Refresh should read all necessary attributes", diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua index 75f9da6961..a6edbf71bd 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_frient_power_energy_current_voltage.lua @@ -356,6 +356,9 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -) +, + { + min_api_version = 15 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter.lua index 571764d8f2..19ad41c700 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter.lua @@ -91,7 +91,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -195,7 +195,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -291,7 +291,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua index f82864cc06..354cffe1e2 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p.lua @@ -76,7 +76,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -156,7 +156,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -240,7 +240,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -259,7 +259,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -305,7 +305,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -324,7 +324,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -347,7 +347,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p_ZM.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p_ZM.lua index ef82322125..530281a13b 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p_ZM.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_1p_ZM.lua @@ -206,6 +206,9 @@ test.register_coroutine_test( }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -) +, + { + min_api_version = 15 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua index 37edda6bd9..5f9d8a3348 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p.lua @@ -73,7 +73,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -93,7 +93,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -153,7 +153,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p_ZM.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p_ZM.lua index 5b959e7c61..ffb31f4b64 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p_ZM.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_2p_ZM.lua @@ -279,6 +279,9 @@ test.register_coroutine_test( }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -) +, + { + min_api_version = 15 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua index ed606d096e..1b227dec62 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p.lua @@ -72,7 +72,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -92,7 +92,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -132,7 +132,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -212,7 +212,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -232,7 +232,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -252,7 +252,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -384,7 +384,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p_ZM.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p_ZM.lua index c4d24f35ba..8aba874223 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p_ZM.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_3p_ZM.lua @@ -353,6 +353,9 @@ test.register_coroutine_test( }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end -) +, + { + min_api_version = 15 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report_sihas.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report_sihas.lua index ece6587f2f..b484ac43ee 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report_sihas.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_consumption_report_sihas.lua @@ -100,7 +100,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -209,7 +209,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -236,7 +236,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -254,7 +254,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -277,7 +277,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua index afb5adda69..39a8bc3d43 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_ezex.lua @@ -93,7 +93,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -132,7 +132,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -166,7 +166,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua index f6cf172d7f..2a4b2db86a 100644 --- a/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua +++ b/drivers/SmartThings/zigbee-power-meter/src/test/test_zigbee_power_meter_frient.lua @@ -193,7 +193,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_aqara_presence_sensor_fp1.lua b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_aqara_presence_sensor_fp1.lua index 557519996e..b04f283c95 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_aqara_presence_sensor_fp1.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_aqara_presence_sensor_fp1.lua @@ -63,7 +63,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -91,7 +91,7 @@ test.register_coroutine_test( data_types.Uint8, updates.preferences["stse.sensitivity"]) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_coroutine_test( data_types.Uint8, 0x01) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -147,7 +147,7 @@ test.register_coroutine_test( data_types.Uint8, updates.preferences["stse.approachDistance"]) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -165,7 +165,7 @@ test.register_coroutine_test( PresenceSensor.presence("present"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_coroutine_test( PresenceSensor.presence("not present"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -206,7 +206,7 @@ test.register_coroutine_test( MovementSensor.movement("noMovement"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_coroutine_test( MovementSensor.movement("noMovement"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -252,7 +252,7 @@ test.register_coroutine_test( MovementSensor.movement("noMovement"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_coroutine_test( MovementSensor.movement("noMovement"))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_st_arrival_sensor_v1.lua b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_st_arrival_sensor_v1.lua index 9124534739..411dddda5a 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_st_arrival_sensor_v1.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_st_arrival_sensor_v1.lua @@ -98,7 +98,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_coroutine_test( add_device_after_switch_over() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -160,7 +160,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("present"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -207,7 +207,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("not present")) ) end, { - test_init = function() end, min_api_version = 17 + test_init = function() end, min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_zigbee_presence_sensor.lua b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_zigbee_presence_sensor.lua index 53843e0014..a688b8026b 100644 --- a/drivers/SmartThings/zigbee-presence-sensor/src/test/test_zigbee_presence_sensor.lua +++ b/drivers/SmartThings/zigbee-presence-sensor/src/test/test_zigbee_presence_sensor.lua @@ -79,7 +79,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_coroutine_test( add_device_after_switch_over() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -214,7 +214,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("not present")) ) end, { - test_init = function() end, min_api_version = 17 + test_init = function() end, min_api_version = 14 } ) @@ -245,7 +245,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_simple_device:generate_test_message("main", capabilities.presenceSensor.presence("not present")) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -280,7 +280,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -334,7 +334,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -349,7 +349,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -397,7 +397,7 @@ test.register_coroutine_test( mock_device_str_interval:generate_test_message("main", capabilities.presenceSensor.presence("not present")) ) end, - { test_init = function() end, min_api_version = 17 + { test_init = function() end, min_api_version = 14 } ) @@ -413,7 +413,7 @@ test.register_coroutine_test( mock_device_nil_interval:generate_test_message("main", capabilities.presenceSensor.presence("not present")) ) end, - { test_init = function() end, min_api_version = 17 + { test_init = function() end, min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua b/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua index 30a6270b8a..5df7aea4a8 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua @@ -52,7 +52,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -134,7 +134,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -153,7 +153,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -191,7 +191,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -210,7 +210,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-range-extender/src/test/test_zigbee_extend.lua b/drivers/SmartThings/zigbee-range-extender/src/test/test_zigbee_extend.lua index 59156dd798..4fddd85ec1 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/test/test_zigbee_extend.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/test/test_zigbee_extend.lua @@ -30,7 +30,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-sensor/src/test/test_zigbee_sensor.lua b/drivers/SmartThings/zigbee-sensor/src/test/test_zigbee_sensor.lua index 32d4762d4b..40ea0c0b4e 100644 --- a/drivers/SmartThings/zigbee-sensor/src/test/test_zigbee_sensor.lua +++ b/drivers/SmartThings/zigbee-sensor/src/test/test_zigbee_sensor.lua @@ -121,7 +121,7 @@ test.register_coroutine_test( mock_device_generic_sensor:expect_metadata_update({profile = ZIGBEE_GENERIC_CONTACT_SENSOR_PROFILE}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_coroutine_test( mock_device_generic_sensor:expect_metadata_update({profile = ZIGBEE_GENERIC_MOTION_SENSOR_PROFILE}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( mock_device_motion_illuminance:expect_metadata_update({profile = ZIGBEE_GENERIC_MOTION_ILLUMINANCE_PROFILE}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_coroutine_test( mock_device_generic_sensor:expect_metadata_update({profile = ZIGBEE_GENERIC_WATERLEAK_SENSOR_PROFILE}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -174,7 +174,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -212,7 +212,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -231,7 +231,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -269,7 +269,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -290,7 +290,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -311,7 +311,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -330,7 +330,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -349,7 +349,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -368,7 +368,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -387,7 +387,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -406,7 +406,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -425,7 +425,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -447,7 +447,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -466,7 +466,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -485,7 +485,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -504,7 +504,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -523,7 +523,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -542,7 +542,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -561,7 +561,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -674,7 +674,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -694,7 +694,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device_motion_sensor.id, ZoneStatusAttribute:read(mock_device_motion_sensor) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -714,7 +714,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device_motion_illuminance.id, ZoneStatusAttribute:read(mock_device_motion_illuminance) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -734,7 +734,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device_waterleak_sensor.id, ZoneStatusAttribute:read(mock_device_waterleak_sensor) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -808,7 +808,7 @@ test.register_coroutine_test( mock_device_contact_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -874,7 +874,7 @@ test.register_coroutine_test( mock_device_motion_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -940,7 +940,7 @@ test.register_coroutine_test( mock_device_motion_illuminance:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1007,7 +1007,7 @@ test.register_coroutine_test( mock_device_waterleak_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua index 4d0486c6fd..3ecee4b16d 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren.lua @@ -259,7 +259,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -379,7 +379,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -410,7 +410,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -440,7 +440,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -456,7 +456,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -516,7 +516,7 @@ test.register_coroutine_test( get_siren_OFF_commands() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -602,7 +602,7 @@ test.register_coroutine_test( get_siren_OFF_commands() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -681,7 +681,7 @@ test.register_coroutine_test( get_siren_OFF_commands(expectedWarningDuration) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -708,7 +708,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -734,7 +734,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -779,7 +779,7 @@ test.register_coroutine_test( get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -824,7 +824,7 @@ test.register_coroutine_test( get_squawk_command_older_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -881,7 +881,7 @@ test.register_coroutine_test( get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -915,7 +915,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -962,7 +962,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -981,7 +981,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1000,7 +1000,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1019,7 +1019,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1038,7 +1038,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1057,7 +1057,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1103,7 +1103,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1121,7 +1121,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1182,7 +1182,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua index 70854ca7e7..cfa4e8b8a5 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_frient_siren_tamper.lua @@ -277,7 +277,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -404,7 +404,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -435,7 +435,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -465,7 +465,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -481,7 +481,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -542,7 +542,7 @@ test.register_coroutine_test( get_siren_OFF_commands() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -628,7 +628,7 @@ test.register_coroutine_test( get_siren_OFF_commands() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -707,7 +707,7 @@ test.register_coroutine_test( get_siren_OFF_commands(expectedWarningDuration) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -733,7 +733,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -759,7 +759,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -804,7 +804,7 @@ test.register_coroutine_test( get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -849,7 +849,7 @@ test.register_coroutine_test( get_squawk_command_older_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -906,7 +906,7 @@ test.register_coroutine_test( get_squawk_command_new_fw(SquawkMode.SOUND_FOR_SYSTEM_IS_DISARMED, IaswdLevel.MEDIUM_LEVEL) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -940,7 +940,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -989,7 +989,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1014,7 +1014,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1039,7 +1039,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1058,7 +1058,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1077,7 +1077,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1096,7 +1096,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1134,7 +1134,7 @@ test.register_coroutine_test( { test_init = function() test.mock_device.add_test_device(mock_device_112) end, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua b/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua index f7b13d4895..70eccc578f 100644 --- a/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua +++ b/drivers/SmartThings/zigbee-siren/src/test/test_ozom_siren.lua @@ -62,7 +62,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_gas_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_gas_detector.lua index c15b0e4d62..47e10b7923 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_gas_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_gas_detector.lua @@ -65,7 +65,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", lifeTimeReport.lifeTimeState.normal())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_coroutine_test( capabilities.gasDetector.gas.detected())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_coroutine_test( capabilities.gasDetector.gas.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( capabilities.audioMute.mute.muted())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( capabilities.audioMute.mute.unmuted())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_coroutine_test( PRIVATE_MUTE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_coroutine_test( capabilities.audioMute.mute.muted())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -206,7 +206,7 @@ test.register_coroutine_test( selfCheck.selfCheckState.selfCheckCompleted())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -223,7 +223,7 @@ test.register_coroutine_test( PRIVATE_SELF_CHECK_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_coroutine_test( lifeTimeReport.lifeTimeState.endOfLife())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -261,7 +261,7 @@ test.register_coroutine_test( lifeTimeReport.lifeTimeState.normal())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -279,7 +279,7 @@ test.register_coroutine_test( sensitivityAdjustment.sensitivityAdjustment.Low())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -297,7 +297,7 @@ test.register_coroutine_test( selfCheck.selfCheckState.idle())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -315,7 +315,7 @@ test.register_coroutine_test( sensitivityAdjustment.sensitivityAdjustment.High())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -349,7 +349,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -384,7 +384,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_smoke_detector.lua index eaee68d41e..31751312c7 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_smoke_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_aqara_smoke_detector.lua @@ -51,7 +51,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -82,7 +82,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_coroutine_test( capabilities.smokeDetector.smoke.detected())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_coroutine_test( capabilities.audioMute.mute.muted())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -132,7 +132,7 @@ test.register_coroutine_test( PRIVATE_MUTE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -150,7 +150,7 @@ test.register_coroutine_test( selfCheck.selfCheckState.selfCheckCompleted())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -165,7 +165,7 @@ test.register_coroutine_test( PRIVATE_SELF_CHECK_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_coroutine_test( capabilities.smokeDetector.smoke.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ test.register_coroutine_test( capabilities.audioMute.mute.unmuted())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( selfCheck.selfCheckState.idle())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -238,7 +238,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua index 7f1a7e5f3a..6512e3504f 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_heat_detector.lua @@ -76,7 +76,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -217,7 +217,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -236,7 +236,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -255,7 +255,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -276,7 +276,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -303,7 +303,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -345,7 +345,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -382,7 +382,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -418,7 +418,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -455,7 +455,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -491,7 +491,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -528,7 +528,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -605,7 +605,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -624,7 +624,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -643,7 +643,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua index 7596148a66..f9a60d24c4 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_frient_smoke_detector.lua @@ -83,7 +83,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -224,7 +224,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -264,7 +264,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -285,7 +285,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -312,7 +312,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -391,7 +391,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -428,7 +428,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -464,7 +464,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -501,7 +501,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -537,7 +537,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -574,7 +574,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -651,7 +651,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -693,7 +693,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -712,7 +712,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -731,7 +731,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -747,7 +747,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_zigbee_smoke_detector.lua b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_zigbee_smoke_detector.lua index f0f9b8259e..1afb849207 100644 --- a/drivers/SmartThings/zigbee-smoke-detector/src/test/test_zigbee_smoke_detector.lua +++ b/drivers/SmartThings/zigbee-smoke-detector/src/test/test_zigbee_smoke_detector.lua @@ -45,7 +45,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -72,7 +72,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -126,7 +126,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -245,7 +245,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -284,7 +284,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-sound-sensor/src/test/test_zigbee_sound_sensor.lua b/drivers/SmartThings/zigbee-sound-sensor/src/test/test_zigbee_sound_sensor.lua index 11a251b1d7..90e1369935 100644 --- a/drivers/SmartThings/zigbee-sound-sensor/src/test/test_zigbee_sound_sensor.lua +++ b/drivers/SmartThings/zigbee-sound-sensor/src/test/test_zigbee_sound_sensor.lua @@ -123,7 +123,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -191,7 +191,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -210,7 +210,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -248,7 +248,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -267,7 +267,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -294,7 +294,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -318,7 +318,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua index 8826ca535d..1332185693 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_all_capability_zigbee_bulb.lua @@ -103,7 +103,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -236,7 +236,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -593,7 +593,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17, + min_api_version = 15, max_api_version = 19 } ) @@ -790,7 +790,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -818,7 +818,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -845,7 +845,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -872,7 +872,7 @@ test.register_coroutine_test( test_init = function() -- no op to override auto device add on startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -909,7 +909,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -928,7 +928,7 @@ test.register_coroutine_test( mock_device:expect_native_attr_handler_registration("colorTemperature", "colorTemperature") end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -962,7 +962,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 14.0, unit = "kWh" }))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua index cc3e86d218..f1dc9e1ea4 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_led_bulb.lua @@ -54,7 +54,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -109,7 +109,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -204,7 +204,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( RESTORE_POWER_STATE_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua index a80bf1dcfc..16ceec4e24 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_light.lua @@ -56,7 +56,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ minimum = 2700, maximum = 6000 }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -211,7 +211,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -226,7 +226,7 @@ test.register_coroutine_test( RESTORE_POWER_STATE_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -241,7 +241,7 @@ test.register_coroutine_test( TURN_OFF_INDICATOR_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -254,7 +254,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnTransitionTime:write(mock_device, 10) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -267,7 +267,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OffTransitionTime:write(mock_device, 10) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -302,7 +302,7 @@ test.register_coroutine_test( test_init = function() test.mock_device.add_test_device(mock_device_cwacn1) end, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug.lua index 6e8ddba455..cffacc4451 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug.lua @@ -78,7 +78,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(ENERGY_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -156,7 +156,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -223,7 +223,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(ENERGY_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -258,7 +258,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -273,7 +273,7 @@ test.register_coroutine_test( RESTORE_POWER_STATE_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -288,7 +288,7 @@ test.register_coroutine_test( MFG_CODE, data_types.SinglePrecisionFloat, SinglePrecisionFloat(0, 6, 0.5625)) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -317,7 +317,7 @@ test.register_coroutine_test( SimpleMetering.attributes.CurrentSummationDelivered:read(mock_standard) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -343,7 +343,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -368,7 +368,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug_t1.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug_t1.lua index 9476f41345..21c697ed1c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug_t1.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_smart_plug_t1.lua @@ -79,7 +79,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -121,7 +121,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -140,7 +140,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(ENERGY_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -174,7 +174,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -224,7 +224,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(ENERGY_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -259,7 +259,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -274,7 +274,7 @@ test.register_coroutine_test( RESTORE_POWER_STATE_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -289,7 +289,7 @@ test.register_coroutine_test( MFG_CODE, data_types.SinglePrecisionFloat, SinglePrecisionFloat(0, 6, 0.5625)) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -304,7 +304,7 @@ test.register_coroutine_test( RESTORE_TURN_OFF_INDICATOR_LIGHT_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_coroutine_test( SimpleMetering.attributes.CurrentSummationDelivered:read(mock_standard) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -359,7 +359,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -384,7 +384,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module.lua index 4779073041..93e21c4844 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module.lua @@ -56,7 +56,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -90,7 +90,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -104,7 +104,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -140,7 +140,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(ENERGY_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -182,7 +182,7 @@ test.register_coroutine_test( RESTORE_POWER_STATE_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_coroutine_test( ELECTRIC_SWITCH_TYPE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module_no_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module_no_power.lua index 8d3135b463..14a816183d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module_no_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_module_no_power.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -62,7 +62,7 @@ test.register_coroutine_test( OnOff.attributes.OnOff:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -75,7 +75,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -88,7 +88,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_coroutine_test( RESTORE_POWER_STATE_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_coroutine_test( ELECTRIC_SWITCH_TYPE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua index 4e7ffaa38a..4f12a94a55 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua @@ -99,7 +99,7 @@ test.register_coroutine_test( { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_coroutine_test( { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -150,7 +150,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -164,7 +164,7 @@ test.register_coroutine_test( OnOff.attributes.OnOff:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.on())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.switch.switch.on())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.switch.switch.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -230,7 +230,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x02) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -258,7 +258,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -272,7 +272,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x02) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -289,7 +289,7 @@ test.register_coroutine_test( capabilities.button.button.pushed({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -306,7 +306,7 @@ test.register_coroutine_test( capabilities.button.button.pushed({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -323,7 +323,7 @@ test.register_coroutine_test( RESTORE_POWER_STATE_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -338,7 +338,7 @@ test.register_coroutine_test( CHANGE_TO_WIRELESS_SWITCH_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua index b6eb4a320e..ffe37bb293 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua @@ -86,7 +86,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -121,7 +121,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(ENERGY_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -189,7 +189,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(POWER_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -217,7 +217,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x02) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -231,7 +231,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -245,7 +245,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x02) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -262,7 +262,7 @@ test.register_coroutine_test( capabilities.button.button.pushed({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -279,7 +279,7 @@ test.register_coroutine_test( capabilities.button.button.pushed({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(ENERGY_METER_ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -328,7 +328,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -343,7 +343,7 @@ test.register_coroutine_test( RESTORE_POWER_STATE_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, true) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -358,7 +358,7 @@ test.register_coroutine_test( CHANGE_TO_WIRELESS_SWITCH_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_wall_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_wall_switch.lua index a0a7e9b924..7efbeefe78 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_wall_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_wall_switch.lua @@ -60,7 +60,7 @@ test.register_coroutine_test( mock_device:expect_native_attr_handler_registration("switch", "switch") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_coroutine_test( mock_device:expect_native_attr_handler_registration("switch", "switch") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_coroutine_test( mock_device:expect_native_attr_handler_registration("switch", "switch") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_coroutine_test( mock_device:expect_native_attr_handler_registration("switch", "switch") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x02) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x02) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_coroutine_test( capabilities.button.button.pushed({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -189,7 +189,7 @@ test.register_coroutine_test( capabilities.button.button.pushed({ state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ test.register_coroutine_test( CHANGE_TO_WIRELESS_SWITCH_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua index 3adf489318..4b16a32bb5 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aurora_relay.lua @@ -42,7 +42,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, SimpleMetering.attributes.InstantaneousDemand:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -69,7 +69,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_bad_data_type.lua b/drivers/SmartThings/zigbee-switch/src/test/test_bad_data_type.lua index 36ac4d5fa0..296ab9a904 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_bad_data_type.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_bad_data_type.lua @@ -43,7 +43,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua b/drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua index 189f64715a..569c5c6b98 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_bad_device_kind.lua @@ -33,7 +33,7 @@ test.register_coroutine_test("zwave_device_handled", function() test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -45,7 +45,7 @@ test.register_message_test( message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua index 66af249b0c..b8cfb47e41 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_cree_bulb.lua @@ -66,7 +66,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -77,7 +77,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.switchLevel.level(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -90,7 +90,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -104,7 +104,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua index d1bebb68d5..5651b0086b 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_duragreen_color_temp_bulb.lua @@ -143,7 +143,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -238,7 +238,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -269,7 +269,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -291,7 +291,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_enbrighten_metering_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_enbrighten_metering_dimmer.lua index f28bb56e37..86d654a053 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_enbrighten_metering_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_enbrighten_metering_dimmer.lua @@ -130,7 +130,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -157,7 +157,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -184,7 +184,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -214,7 +214,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -263,7 +263,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -282,7 +282,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua b/drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua index b410872075..f1e568bc7f 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_frient_IO_module.lua @@ -522,7 +522,7 @@ test.register_coroutine_test( assert(parent_native, "expected parent device to register native switch handler for input 3") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -556,7 +556,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -616,7 +616,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -640,7 +640,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -691,7 +691,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua index 2a86e1ed19..90cd1524bd 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_frient_switch.lua @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test("Current divisor, multiplier, summation should be han }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -153,7 +153,7 @@ test.register_coroutine_test("Refresh command should read all necessary attribut ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -180,7 +180,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -207,7 +207,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -239,7 +239,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -264,7 +264,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -277,7 +277,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -341,7 +341,7 @@ test.register_coroutine_test("doConfigure should send bind request, read attribu mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -357,7 +357,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.powerSource.powerSource.unknown())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_ge_link_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_ge_link_bulb.lua index d35bbf4f35..94104f74b7 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_ge_link_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_ge_link_bulb.lua @@ -47,7 +47,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -64,7 +64,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 50) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -79,7 +79,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 0) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -189,7 +189,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.OnOffTransitionTime:write(mock_device, 50) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua index 7982403b8c..13184c37d3 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_hanssem_switch.lua @@ -128,7 +128,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -161,7 +161,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -194,7 +194,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -227,7 +227,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -260,7 +260,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -293,7 +293,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -326,7 +326,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -359,7 +359,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -392,7 +392,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -425,7 +425,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -458,7 +458,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -491,7 +491,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -523,7 +523,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -550,7 +550,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -577,7 +577,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -604,7 +604,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -631,7 +631,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -658,7 +658,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -685,7 +685,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -712,7 +712,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -739,7 +739,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -766,7 +766,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -793,7 +793,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -820,7 +820,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -870,7 +870,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua index b2b7019dd0..3dcee92797 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn.lua @@ -105,7 +105,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -289,7 +289,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -321,7 +321,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -359,7 +359,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -382,7 +382,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -405,7 +405,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -427,7 +427,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -448,7 +448,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -497,7 +497,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -556,7 +556,7 @@ test.register_coroutine_test( mock_inovelli_vzm30_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua index 3c839f571f..8be1b0ee0a 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_child.lua @@ -91,7 +91,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -180,7 +180,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -239,7 +239,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -302,7 +302,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -365,7 +365,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua index 6c6d633d70..fa6bf5202f 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm30_sn_preferences.lua @@ -63,7 +63,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -88,7 +88,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -160,7 +160,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -184,7 +184,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -227,7 +227,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm30_sn:generate_info_changed({preferences = {notificationChild = true}})) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua index 6c0da0b158..1549b80a41 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -73,7 +73,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -121,7 +121,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -218,7 +218,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -288,7 +288,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -310,7 +310,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -332,7 +332,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -381,7 +381,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -433,7 +433,7 @@ test.register_coroutine_test( mock_inovelli_vzm31_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua index fd7f718ff9..a3b1717c04 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua @@ -80,7 +80,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -135,7 +135,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -228,7 +228,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -291,7 +291,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -354,7 +354,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua index 3823d4e010..56f526c2cd 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua @@ -52,7 +52,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -77,7 +77,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {notificationChild = true}})) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index ee21044bea..80f7a765a7 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -84,7 +84,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -166,7 +166,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -263,7 +263,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -295,7 +295,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -356,7 +356,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -379,7 +379,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -402,7 +402,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -425,7 +425,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -445,7 +445,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -494,7 +494,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -549,7 +549,7 @@ test.register_coroutine_test( mock_inovelli_vzm32_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua index 7173463e16..544ee73aea 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua @@ -80,7 +80,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -135,7 +135,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -228,7 +228,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -291,7 +291,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -354,7 +354,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua index 9939926ff3..a630e54efe 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua @@ -52,7 +52,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -77,7 +77,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {notificationChild = true}})) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -174,7 +174,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua index 6d1b78e806..14df41e010 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_jasco_switch.lua @@ -56,7 +56,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -83,7 +83,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -126,7 +126,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua b/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua index 5c7df6e762..5a2e92e8a3 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_laisiao_bath_heather.lua @@ -50,7 +50,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -70,7 +70,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -90,7 +90,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -130,7 +130,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -150,7 +150,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -212,7 +212,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -232,7 +232,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -252,7 +252,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -272,7 +272,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -292,7 +292,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -312,7 +312,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -332,7 +332,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -352,7 +352,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -365,7 +365,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x02) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -378,7 +378,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x03) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -391,7 +391,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x04) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -404,7 +404,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x05) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -417,7 +417,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x06) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -430,7 +430,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x07) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -443,7 +443,7 @@ test.register_coroutine_test( OnOff.server.commands.On(mock_device):to_endpoint(0x08) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -456,7 +456,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x01) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -469,7 +469,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x02) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -482,7 +482,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x03) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -495,7 +495,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x04) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -508,7 +508,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x05) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -521,7 +521,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x06) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -534,7 +534,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x07) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -547,7 +547,7 @@ test.register_coroutine_test( OnOff.server.commands.Off(mock_device):to_endpoint(0x08) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -567,7 +567,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_ledvance_metering_plug.lua b/drivers/SmartThings/zigbee-switch/src/test/test_ledvance_metering_plug.lua index ebdcae468e..0968fa7c5e 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_ledvance_metering_plug.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_ledvance_metering_plug.lua @@ -55,7 +55,7 @@ test.register_coroutine_test( assert(mock_device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == 100) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -70,7 +70,7 @@ test.register_coroutine_test( assert(mock_device:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == 1000) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -85,7 +85,7 @@ test.register_coroutine_test( assert(mock_device_eu_em_t:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == 100) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -100,7 +100,7 @@ test.register_coroutine_test( assert(mock_device_eu_em_t:get_field(zigbee_constants.SIMPLE_METERING_DIVISOR_KEY) == 1000) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua index b6b5adade5..9e1ccb8d5e 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch.lua @@ -39,7 +39,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -59,7 +59,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) test.register_message_test( @@ -78,7 +78,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_no_master.lua b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_no_master.lua index 2ec4c6eb86..1d5d8b712a 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_no_master.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_no_master.lua @@ -105,7 +105,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -171,7 +171,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -237,7 +237,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -270,7 +270,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -297,7 +297,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -324,7 +324,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -351,7 +351,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -378,7 +378,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -405,7 +405,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -432,7 +432,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -511,7 +511,7 @@ test.register_coroutine_test( mock_base_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -553,7 +553,7 @@ test.register_coroutine_test( test_init = function() test.mock_device.add_test_device(mock_non_mns_device) end, - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_power.lua index 788c5477c5..5df2466cb2 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_multi_switch_power.lua @@ -117,7 +117,7 @@ test.register_coroutine_test( test_init = function() -- no op to avoid auto device add and immediate init event on driver startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( test_init = function() -- no op to avoid auto device add and immediate init event on driver startup end, - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -231,7 +231,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -259,7 +259,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -287,7 +287,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -315,7 +315,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -345,7 +345,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -375,7 +375,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -402,7 +402,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -429,7 +429,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -456,7 +456,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -483,7 +483,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -509,7 +509,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -528,7 +528,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_on_off_zigbee_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_on_off_zigbee_bulb.lua index 7a1af4a260..062bd8fc6c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_on_off_zigbee_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_on_off_zigbee_bulb.lua @@ -47,7 +47,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -75,7 +75,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -103,7 +103,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -132,7 +132,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -175,7 +175,7 @@ test.register_coroutine_test( mock_simple_device:expect_metadata_update({provisioning_state = "PROVISIONED"}) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_osram_iqbr30_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_osram_iqbr30_light.lua index 8727de3c35..4d846e8380 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_osram_iqbr30_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_osram_iqbr30_light.lua @@ -65,7 +65,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.switchLevel.level(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_message_test( }, { inner_block_ordering = "RELAXED", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua b/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua index 29a53accff..a09436c9ce 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_osram_light.lua @@ -65,7 +65,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.switchLevel.level(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua index 06c20b34e5..bac6c6715c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_rgb_bulb.lua @@ -67,7 +67,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -200,7 +200,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua index 4a83d8b920..604780cd9c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_rgbw_bulb.lua @@ -165,7 +165,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -221,7 +221,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -248,7 +248,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -304,7 +304,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -330,7 +330,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -346,7 +346,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_2-wire_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_2-wire_dimmer.lua index cfbfa42b50..462a607694 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_2-wire_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_2-wire_dimmer.lua @@ -56,7 +56,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -83,7 +83,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -111,7 +111,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -131,7 +131,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_knob_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_knob_dimmer.lua index 1bfa694b5b..cf64a89595 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_knob_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_robb_smarrt_knob_dimmer.lua @@ -56,7 +56,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -83,7 +83,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -111,7 +111,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -132,7 +132,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua index 80241b4ca9..7d633129ec 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_color_temp_bulb.lua @@ -143,7 +143,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -238,7 +238,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -269,7 +269,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_dimmer_bulb_with_motion_sensor.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_dimmer_bulb_with_motion_sensor.lua index aa4e56653c..6ba6eef2b2 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sengled_dimmer_bulb_with_motion_sensor.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sengled_dimmer_bulb_with_motion_sensor.lua @@ -69,7 +69,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -83,7 +83,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -217,7 +217,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua index 4de1a70303..a16d47a4b0 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_dimmer.lua @@ -73,7 +73,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -109,7 +109,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -184,7 +184,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -214,7 +214,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -240,7 +240,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -280,7 +280,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -316,7 +316,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -360,7 +360,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua index 9d9b33df99..28bd04a664 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_sinope_switch.lua @@ -57,7 +57,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -111,7 +111,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua index 4ff3fb2afa..2a26c95e3c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_switch_power.lua @@ -125,7 +125,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -180,7 +180,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -242,7 +242,7 @@ test.register_coroutine_test( mock_aurora_relay_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -312,7 +312,7 @@ test.register_coroutine_test( mock_vimar_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua index ac3a78a982..7754d6281e 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi.lua @@ -78,7 +78,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi_switch.lua index b6b5adade5..9e1ccb8d5e 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_tuya_multi_switch.lua @@ -39,7 +39,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -59,7 +59,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) test.register_message_test( @@ -78,7 +78,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua index 8bbe6bad68..f28334b661 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_wallhero_switch.lua @@ -182,7 +182,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -215,7 +215,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -248,7 +248,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -281,7 +281,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -314,7 +314,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -347,7 +347,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -380,7 +380,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -413,7 +413,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -428,7 +428,7 @@ test.register_coroutine_test( 0x6000, 0x1235, data_types.Uint8, 0x01) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -443,7 +443,7 @@ test.register_coroutine_test( 0x6000, 0x1235, data_types.Uint8, 0x00) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -475,7 +475,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -502,7 +502,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -529,7 +529,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -556,7 +556,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -584,7 +584,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -611,7 +611,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -638,7 +638,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -665,7 +665,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -682,7 +682,7 @@ test.register_coroutine_test( ))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -698,7 +698,7 @@ test.register_coroutine_test( ))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -714,7 +714,7 @@ test.register_coroutine_test( ))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -730,7 +730,7 @@ test.register_coroutine_test( ))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -794,7 +794,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -808,7 +808,7 @@ test.register_coroutine_test( capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua index 8a0db9d65c..1524a8aa85 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_white_color_temp_bulb.lua @@ -144,7 +144,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -239,7 +239,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -270,7 +270,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua index 5f29f49c2b..889f4f15d6 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_yanmi_switch.lua @@ -101,7 +101,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -134,7 +134,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -267,7 +267,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -300,7 +300,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -327,7 +327,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -354,7 +354,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -384,7 +384,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -411,7 +411,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -438,7 +438,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_ezex_switch.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_ezex_switch.lua index d2b73dd4b1..cfcab8e20e 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_ezex_switch.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_ezex_switch.lua @@ -54,7 +54,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -127,7 +127,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -146,7 +146,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_power_consumption_report.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_power_consumption_report.lua index e265fa13dc..1b4d46446b 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_power_consumption_report.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_power_consumption_report.lua @@ -66,7 +66,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -85,7 +85,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -153,7 +153,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_rexense.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_rexense.lua index a616744baf..ff732a9589 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_rexense.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zigbee_metering_plug_rexense.lua @@ -49,7 +49,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua index f6842b077f..20f6d458a2 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_color_temp_bulb.lua @@ -66,7 +66,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -115,7 +115,7 @@ test.register_coroutine_test( test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "polling") end, - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -156,7 +156,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -197,7 +197,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -238,7 +238,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -281,7 +281,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua index 2e688c0a86..7738dc659b 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer.lua @@ -67,7 +67,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -78,7 +78,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main",capabilities.switchLevel.level(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -91,7 +91,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_coroutine_test( test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(5*60, "interval", "polling") end, - min_api_version = 17 + min_api_version = 14 } ) @@ -171,7 +171,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua index ec10a66a62..195e0eb469 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_dimmer_bulb.lua @@ -67,7 +67,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -80,7 +80,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -93,7 +93,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_coroutine_test( test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "polling") end, - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -178,7 +178,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgb_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgb_bulb.lua index bbbda5660d..2eeffc86aa 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgb_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgb_bulb.lua @@ -85,7 +85,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_coroutine_test( test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(30, "interval", "polling") end, - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -185,7 +185,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -286,7 +286,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -365,7 +365,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -406,7 +406,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -447,7 +447,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -474,7 +474,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentY:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -489,7 +489,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorControl.saturation(65))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -504,7 +504,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorControl.saturation(65))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -519,7 +519,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorControl.saturation(65))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -534,7 +534,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorControl.saturation(65))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua index 4e2ad43b06..0a5d739a48 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_zll_rgbw_bulb.lua @@ -195,7 +195,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -248,7 +248,7 @@ test.register_coroutine_test( test.mock_device.add_test_device(mock_device) test.timer.__create_and_queue_test_time_advance_timer(5*60, "interval", "polling") end, - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -354,7 +354,7 @@ test.register_coroutine_test( end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -407,7 +407,7 @@ test.register_coroutine_test( end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -462,7 +462,7 @@ test.register_coroutine_test( end, { - min_api_version = 17, + min_api_version = 14, max_api_version = 19 } ) @@ -491,7 +491,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentHue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -519,7 +519,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -549,7 +549,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.CurrentSaturation:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua index 8b84e987a0..d6ae23b544 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_aqara_thermostat.lua @@ -77,7 +77,7 @@ test.register_coroutine_test( PRIVATE_THERM0STAT_VALVE_DETECTION_SWITCH_ID, MFG_CODE, data_types.Uint8, 0x01) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -92,7 +92,7 @@ test.register_coroutine_test( PRIVATE_ANTIFREEZE_MODE_TEMPERATURE_SETTING_ID, MFG_CODE, data_types.Uint32, 500) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_coroutine_test( capabilities.hardwareFault.hardwareFault.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -158,7 +158,7 @@ test.register_coroutine_test( valveCalibration.calibrationState.calibrationFailure())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_coroutine_test( invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -258,7 +258,7 @@ test.register_coroutine_test( invisibleCapabilities.invisibleCapabilities({"thermostatHeatingSetpoint"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -286,7 +286,7 @@ test.register_coroutine_test( capabilities.lock.lock.locked())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -305,7 +305,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -366,7 +366,7 @@ test.register_coroutine_test( PRIVATE_VALVE_SWITCH_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -380,7 +380,7 @@ test.register_coroutine_test( PRIVATE_VALVE_CALIBRATION_ID, MFG_CODE, data_types.Uint8, 0x01) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -395,7 +395,7 @@ test.register_coroutine_test( PRIVATE_CHILD_LOCK_ID, MFG_CODE, data_types.Uint8, 0x01) }) end, { - min_api_version = 17 + min_api_version = 14 } ) --]] @@ -470,7 +470,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua index b1942290db..fdf30687aa 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_centralite_thermostat.lua @@ -48,7 +48,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -88,7 +88,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -150,7 +150,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua index 43fe7c3c82..33bda1be3e 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_danfoss_thermostat.lua @@ -44,7 +44,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -55,7 +55,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device_danfoss:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.0, unit = "C"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -66,7 +66,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device_danfoss:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({ value = 21.0, unit = "C"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_coroutine_test( test.socket.zigbee:__queue_receive({ mock_device_danfoss.id, Thermostat.attributes.OccupiedCoolingSetpoint:build_test_attr_report(mock_device_danfoss, 2100) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -121,7 +121,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua index 469f36dec9..25ffd9a224 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_fidure_thermostat.lua @@ -63,7 +63,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -78,7 +78,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua index 8fc6b2944f..7101150397 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_leviton_rc.lua @@ -53,7 +53,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -70,7 +70,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.LocalTemperature:read(mock_device):to_endpoint(ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({value = 21.0, unit = "C"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -92,7 +92,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatFanMode.thermostatFanMode.auto())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -130,7 +130,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatCoolingSetpoint.coolingSetpoint({value = 21.0, unit = "C"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatHeatingSetpoint.heatingSetpoint({value = 21.0, unit = "C"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( test.socket.zigbee:__queue_receive({ mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:build_test_attr_report(mock_device, 2100) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_coroutine_test( test.socket.zigbee:__queue_receive({ mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:build_test_attr_report(mock_device, 2100) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -185,7 +185,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 2100):to_endpoint(ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.OccupiedCoolingSetpoint:write(mock_device, 2100):to_endpoint(ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.SystemMode:write(mock_device, 3):to_endpoint(ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, FanControl.attributes.FanMode:write(mock_device, 5):to_endpoint(ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send( { mock_device.id, Thermostat.attributes.LocalTemperature:read(mock_device):to_endpoint(ENDPOINT) }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua index d1ae50109d..b351d725bd 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_popp_thermostat.lua @@ -69,7 +69,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -122,7 +122,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -254,7 +254,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -291,7 +291,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -319,7 +319,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -356,7 +356,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -393,7 +393,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -410,7 +410,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.thermostatMode.thermostatMode.eco())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -462,7 +462,7 @@ test.register_coroutine_test( )}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -479,7 +479,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureAlarm.temperatureAlarm.cleared())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -496,7 +496,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_resideo_dt300st_m000.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_resideo_dt300st_m000.lua index 71264c3a99..5a0f8591ed 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_resideo_dt300st_m000.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_resideo_dt300st_m000.lua @@ -102,7 +102,7 @@ test.register_coroutine_test("Configure should configure all necessary attribute }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_coroutine_test("Refresh should read all necessary attributes", fun end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_coroutine_test("Temperature reporting should create the appropriat }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -156,7 +156,7 @@ test.register_coroutine_test("Thermostat mode reporting should create the approp .thermostatMode.heat())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_coroutine_test("ControlSequenceOfOperation reporting should create }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_coroutine_test("OccupiedHeatingSetpoint reporting shoulb create th }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_coroutine_test("Setting the heating setpoint should generate the a Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_device, 2100)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -218,7 +218,7 @@ test.register_coroutine_test("Setting the thermostat mode to away should generat Thermostat.attributes.SystemMode.OFF)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -257,7 +257,7 @@ test.register_coroutine_test("Refresh should read all necessary attributes with end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -271,7 +271,7 @@ test.register_coroutine_test("Temperature reporting should create the appropriat }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -288,7 +288,7 @@ test.register_coroutine_test("Thermostat mode reporting should create the approp .thermostatMode.heat())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -304,7 +304,7 @@ test.register_coroutine_test("ControlSequenceOfOperation reporting should create }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -319,7 +319,7 @@ test.register_coroutine_test("OccupiedHeatingSetpoint reporting shoulb create th }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -334,7 +334,7 @@ test.register_coroutine_test("Setting the heating setpoint should generate the a Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_first_child, 2100)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -350,7 +350,7 @@ test.register_coroutine_test("Setting the thermostat mode to away should generat Thermostat.attributes.SystemMode.OFF)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -366,7 +366,7 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -389,7 +389,7 @@ test.register_coroutine_test("Refresh should read all necessary attributes with end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -403,7 +403,7 @@ test.register_coroutine_test("Temperature reporting should create the appropriat }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -420,7 +420,7 @@ test.register_coroutine_test("Thermostat mode reporting should create the approp .thermostatMode.heat())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -436,7 +436,7 @@ test.register_coroutine_test("ControlSequenceOfOperation reporting should create }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -451,7 +451,7 @@ test.register_coroutine_test("OccupiedHeatingSetpoint reporting shoulb create th }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -466,7 +466,7 @@ test.register_coroutine_test("Setting the heating setpoint should generate the a Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_second_child, 2100)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -482,7 +482,7 @@ test.register_coroutine_test("Setting the thermostat mode to away should generat Thermostat.attributes.SystemMode.OFF)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -498,7 +498,7 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -521,7 +521,7 @@ test.register_coroutine_test("Refresh should read all necessary attributes with end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -535,7 +535,7 @@ test.register_coroutine_test("Temperature reporting should create the appropriat }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -552,7 +552,7 @@ test.register_coroutine_test("Thermostat mode reporting should create the approp .thermostatMode.heat())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -568,7 +568,7 @@ test.register_coroutine_test("ControlSequenceOfOperation reporting should create }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -583,7 +583,7 @@ test.register_coroutine_test("OccupiedHeatingSetpoint reporting shoulb create th }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -598,7 +598,7 @@ test.register_coroutine_test("Setting the heating setpoint should generate the a Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_third_child, 2100)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -614,7 +614,7 @@ test.register_coroutine_test("Setting the thermostat mode to away should generat Thermostat.attributes.SystemMode.OFF)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -630,7 +630,7 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -653,7 +653,7 @@ test.register_coroutine_test("Refresh should read all necessary attributes with end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -667,7 +667,7 @@ test.register_coroutine_test("Temperature reporting should create the appropriat }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -684,7 +684,7 @@ test.register_coroutine_test("Thermostat mode reporting should create the approp .thermostatMode.heat())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -700,7 +700,7 @@ test.register_coroutine_test("ControlSequenceOfOperation reporting should create }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -715,7 +715,7 @@ test.register_coroutine_test("OccupiedHeatingSetpoint reporting shoulb create th }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -730,7 +730,7 @@ test.register_coroutine_test("Setting the heating setpoint should generate the a Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_forth_child, 2100)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -746,7 +746,7 @@ test.register_coroutine_test("Setting the thermostat mode to away should generat Thermostat.attributes.SystemMode.OFF)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -762,7 +762,7 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -785,7 +785,7 @@ test.register_coroutine_test("Refresh should read all necessary attributes with end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -799,7 +799,7 @@ test.register_coroutine_test("Temperature reporting should create the appropriat }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -816,7 +816,7 @@ test.register_coroutine_test("Thermostat mode reporting should create the approp .thermostatMode.heat())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -832,7 +832,7 @@ test.register_coroutine_test("ControlSequenceOfOperation reporting should create }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -847,7 +847,7 @@ test.register_coroutine_test("OccupiedHeatingSetpoint reporting shoulb create th }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -862,7 +862,7 @@ test.register_coroutine_test("Setting the heating setpoint should generate the a Thermostat.attributes.OccupiedHeatingSetpoint:write(mock_fifth_child, 2100)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -878,7 +878,7 @@ test.register_coroutine_test("Setting the thermostat mode to away should generat Thermostat.attributes.SystemMode.OFF)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -894,7 +894,7 @@ test.register_coroutine_test("Setting the thermostat mode to heat should generat Thermostat.attributes.SystemMode.HEAT)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -913,7 +913,7 @@ test.register_coroutine_test("ThermostatRunningState reporting shoulb create the capabilities.thermostatOperatingState.thermostatOperatingState({value="fan only"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1300_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1300_thermostat.lua index 271c070df3..8fc50e9706 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1300_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1300_thermostat.lua @@ -47,7 +47,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -66,7 +66,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1400_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1400_thermostat.lua index 55ba5be56c..5d7a679e92 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1400_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_th1400_thermostat.lua @@ -47,7 +47,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -66,7 +66,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua index 2506ca0f2f..a85e50971f 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_sinope_thermostat.lua @@ -47,7 +47,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -66,7 +66,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -182,7 +182,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -212,7 +212,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_ki_zigbee_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_ki_zigbee_thermostat.lua index 470bb1378d..da890c2b11 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_ki_zigbee_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_ki_zigbee_thermostat.lua @@ -59,7 +59,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -78,7 +78,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -126,7 +126,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -150,7 +150,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -210,7 +210,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -242,7 +242,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", ThermostatOperatingState.thermostatOperatingState("idle"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -265,7 +265,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", ThermostatMode.thermostatMode.heat())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -298,7 +298,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -331,7 +331,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -423,7 +423,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -508,7 +508,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -531,7 +531,7 @@ test.register_coroutine_test("Setting the heating setpoint should generate the a }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -563,7 +563,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -588,7 +588,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -615,7 +615,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua index 208949090c..bc0b3933c0 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_stelpro_thermostat.lua @@ -51,7 +51,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -75,7 +75,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -237,7 +237,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -256,7 +256,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -294,7 +294,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -355,7 +355,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -400,7 +400,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -440,7 +440,7 @@ test.register_coroutine_test( test.socket.zigbee:__set_channel_ordering("relaxed") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -485,7 +485,7 @@ test.register_coroutine_test( mock_device_maestro:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -500,7 +500,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({preferences = { lock = 1 } })) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -513,7 +513,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -551,7 +551,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua index afbe34524b..9ba03896e8 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_vimar_thermostat.lua @@ -87,7 +87,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -227,7 +227,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -269,7 +269,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -311,7 +311,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -375,7 +375,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -415,7 +415,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -454,7 +454,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -496,7 +496,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -538,7 +538,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -602,7 +602,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -637,7 +637,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -671,7 +671,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -730,7 +730,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -790,7 +790,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -820,7 +820,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -850,7 +850,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua index c16757366d..ddd240a0e0 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zenwithin_thermostat.lua @@ -49,7 +49,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -69,7 +69,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) test.register_message_test( @@ -88,7 +88,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -211,7 +211,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -291,7 +291,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -426,7 +426,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -461,7 +461,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -476,7 +476,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -519,7 +519,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -534,7 +534,7 @@ test.register_coroutine_test( assert(mock_device:get_field("minHeatSetpoint") == 500, "minHeatSetpoint field not stored correctly") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -549,7 +549,7 @@ test.register_coroutine_test( assert(mock_device:get_field("maxHeatSetpoint") == 3500, "maxHeatSetpoint field not stored correctly") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -564,7 +564,7 @@ test.register_coroutine_test( assert(mock_device:get_field("minCoolSetpoint") == 1600, "minCoolSetpoint field not stored correctly") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -579,7 +579,7 @@ test.register_coroutine_test( assert(mock_device:get_field("maxCoolSetpoint") == 3200, "maxCoolSetpoint field not stored correctly") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -617,7 +617,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -655,7 +655,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -691,7 +691,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -727,7 +727,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -884,7 +884,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua index e954e05a8a..40f8a58e2d 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat.lua @@ -38,7 +38,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -58,7 +58,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -85,7 +85,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -300,7 +300,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -320,7 +320,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -361,7 +361,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -381,7 +381,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -412,7 +412,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -443,7 +443,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -474,7 +474,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -505,7 +505,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -535,7 +535,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -566,7 +566,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -597,7 +597,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -643,7 +643,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -785,7 +785,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -911,7 +911,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -931,7 +931,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua index a93a3a36ee..b258d14727 100644 --- a/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua +++ b/drivers/SmartThings/zigbee-thermostat/src/test/test_zigbee_thermostat_wallhero_3in1.lua @@ -212,7 +212,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -233,7 +233,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -273,7 +273,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -293,7 +293,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -353,7 +353,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -373,7 +373,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -393,7 +393,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -413,7 +413,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -433,7 +433,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -453,7 +453,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -473,7 +473,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -493,7 +493,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -513,7 +513,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -533,7 +533,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -553,7 +553,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -573,7 +573,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -593,7 +593,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -613,7 +613,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -633,7 +633,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -652,7 +652,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -671,7 +671,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -690,7 +690,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -709,7 +709,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -728,7 +728,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -747,7 +747,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -766,7 +766,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -785,7 +785,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -804,7 +804,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -823,7 +823,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -842,7 +842,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -861,7 +861,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -880,7 +880,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -899,7 +899,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -918,7 +918,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -937,7 +937,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -956,7 +956,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -975,7 +975,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -994,7 +994,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1013,7 +1013,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1233,7 +1233,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua index d9db636eab..013e79a828 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua @@ -47,7 +47,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -87,7 +87,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -106,7 +106,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -126,7 +126,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -166,7 +166,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -224,7 +224,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -272,7 +272,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -314,7 +314,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -353,7 +353,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua index 9ba28f751e..c081d0ab40 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_sinope_valve.lua @@ -49,7 +49,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(20)) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(0)) ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -153,7 +153,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.battery.battery(100)) ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua index 72038fdb37..36eb70de1d 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_zigbee_valve.lua @@ -47,7 +47,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -87,7 +87,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -147,7 +147,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -254,7 +254,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -296,7 +296,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -335,7 +335,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-vent/src/test/test_zigbee_vent.lua b/drivers/SmartThings/zigbee-vent/src/test/test_zigbee_vent.lua index 26420c997b..d4781ddfd3 100644 --- a/drivers/SmartThings/zigbee-vent/src/test/test_zigbee_vent.lua +++ b/drivers/SmartThings/zigbee-vent/src/test/test_zigbee_vent.lua @@ -61,7 +61,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, Level.attributes.CurrentLevel:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -241,7 +241,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -299,7 +299,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -320,7 +320,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, Level.commands.MoveToLevelWithOnOff(mock_device, math.floor(83 / 100 * 254), 0xFFFF)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.battery.battery(50))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -345,7 +345,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.atmosphericPressureMeasurement.atmosphericPressure({value = 1, unit = "kPa"}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_aqara_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_aqara_water_leak_sensor.lua index 51be73b5b3..06f558bdcd 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_aqara_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_aqara_water_leak_sensor.lua @@ -53,7 +53,7 @@ test.register_coroutine_test( data_types.Uint8, 1) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -80,7 +80,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -126,7 +126,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_centralite_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_centralite_water_leak_sensor.lua index d92293d30e..a5acbfb81e 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_centralite_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_centralite_water_leak_sensor.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -240,7 +240,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -272,7 +272,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_frient_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_frient_water_leak_sensor.lua index ba6361bc9d..6c0881c715 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_frient_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_frient_water_leak_sensor.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -135,7 +135,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -154,7 +154,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -246,7 +246,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -278,7 +278,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_leaksmart_water.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_leaksmart_water.lua index 140608bfcc..47c17f4c2f 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_leaksmart_water.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_leaksmart_water.lua @@ -42,7 +42,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.waterSensor.water.wet())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -54,7 +54,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.waterSensor.water.dry())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -66,7 +66,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.waterSensor.water.dry())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -78,7 +78,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.waterSensor.water.dry())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -90,7 +90,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.waterSensor.water.dry())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -103,7 +103,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_samjin_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_samjin_water_leak_sensor.lua index 7b28197ad6..bc941ebfdb 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_samjin_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_samjin_water_leak_sensor.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -175,7 +175,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sengled_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sengled_water_leak_sensor.lua index 1e7c70eba1..5c2764ebe7 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sengled_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sengled_water_leak_sensor.lua @@ -45,7 +45,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sinope_zigbee_water.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sinope_zigbee_water.lua index 2a6fd6be79..c7b8f35247 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sinope_zigbee_water.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_sinope_zigbee_water.lua @@ -48,7 +48,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -67,7 +67,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -151,7 +151,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -170,7 +170,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -376,7 +376,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_smartthings_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_smartthings_water_leak_sensor.lua index e550d7d246..bde19b9206 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_smartthings_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_smartthings_water_leak_sensor.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -143,7 +143,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_thirdreality_water_leak_sensor.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_thirdreality_water_leak_sensor.lua index e3946056f4..8056969142 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_thirdreality_water_leak_sensor.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_thirdreality_water_leak_sensor.lua @@ -43,7 +43,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, Basic.attributes.ApplicationVersion:read(mock_device)}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -56,7 +56,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -106,7 +106,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -156,7 +156,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water.lua index bf6bb7c4a9..bda51e2655 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water.lua @@ -46,7 +46,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -154,7 +154,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -184,7 +184,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -346,7 +346,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -409,7 +409,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water_freeze.lua b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water_freeze.lua index a8e951661f..a718ea0b75 100644 --- a/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water_freeze.lua +++ b/drivers/SmartThings/zigbee-water-leak-sensor/src/test/test_zigbee_water_freeze.lua @@ -54,7 +54,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -78,7 +78,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -226,7 +226,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -289,7 +289,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-watering-kit/src/test/test_thirdreality_watering_kit.lua b/drivers/SmartThings/zigbee-watering-kit/src/test/test_thirdreality_watering_kit.lua index 53118b5bff..fdb72d3d0c 100644 --- a/drivers/SmartThings/zigbee-watering-kit/src/test/test_thirdreality_watering_kit.lua +++ b/drivers/SmartThings/zigbee-watering-kit/src/test/test_thirdreality_watering_kit.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.mode.mode("0"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -67,7 +67,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -143,7 +143,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -181,7 +181,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fanSpeed.fanSpeed(10))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -215,7 +215,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fanSpeed.fanSpeed(30))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -232,7 +232,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.mode.mode("4"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_coroutine_test( THIRDREALITY_WATERING_CLUSTER, WATERING_TIME_ATTR, 0x1407, data_types.Uint16, 20) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -256,7 +256,7 @@ test.register_coroutine_test( THIRDREALITY_WATERING_CLUSTER, WATERING_INTERVAL_ATTR, 0x1407, data_types.Uint8, 2) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -294,7 +294,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -311,7 +311,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.fanSpeed.fanSpeed(0))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua index 26a08b1a84..c64116d7fa 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_battery_ikea.lua @@ -56,7 +56,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -78,7 +78,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -100,7 +100,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -133,7 +133,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -186,7 +186,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -213,7 +213,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -232,7 +232,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -273,7 +273,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -312,7 +312,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -351,7 +351,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua index 71dc5c81a4..2653d47555 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_shade_only_HOPOsmart.lua @@ -45,7 +45,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, read_0x0000_messge}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -69,7 +69,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.open())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -154,7 +154,7 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.opening())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.closed())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.closing())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( capabilities.windowShade.windowShade.partially_open())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua index 145876ff64..30bc6bfa5c 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment.lua @@ -55,7 +55,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -132,7 +132,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -181,7 +181,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -209,7 +209,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -272,7 +272,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -298,7 +298,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -367,7 +367,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -401,7 +401,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -456,7 +456,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -491,7 +491,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -526,7 +526,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -556,7 +556,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -588,7 +588,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua index ecf2586a5c..ba5b9c20a5 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua @@ -51,7 +51,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, read_0x0001_messge}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -70,7 +70,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({mock_device.id, read_0x0001_messge}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -94,7 +94,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -121,7 +121,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -143,7 +143,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -160,7 +160,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -194,7 +194,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -211,7 +211,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_coroutine_test( capabilities.mode.mode("Delete upper limit"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_coroutine_test( capabilities.mode.mode("Set the upper limit"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -265,7 +265,7 @@ test.register_coroutine_test( capabilities.mode.mode("Delete lower limit"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -283,7 +283,7 @@ test.register_coroutine_test( capabilities.mode.mode("Set the lower limit"))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( capabilities.hardwareFault.hardwareFault.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -319,7 +319,7 @@ test.register_coroutine_test( capabilities.hardwareFault.hardwareFault.detected())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -340,7 +340,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -361,7 +361,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua index 08c71ec85a..23b54edab5 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara.lua @@ -143,7 +143,7 @@ test.register_coroutine_test( PREF_SOFT_TOUCH_ON) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -178,7 +178,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -200,7 +200,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -222,7 +222,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -263,7 +263,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -282,7 +282,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -348,7 +348,7 @@ test.register_coroutine_test( deviceInitialization.initializedState.initialized())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -380,7 +380,7 @@ test.register_coroutine_test( PREF_SOFT_TOUCH_OFF) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -404,7 +404,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -434,7 +434,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -464,7 +464,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -494,7 +494,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -514,7 +514,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -544,7 +544,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -576,7 +576,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -609,7 +609,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -631,7 +631,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -653,7 +653,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua index 0177c71467..82d5fac234 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_curtain_driver_e1.lua @@ -111,7 +111,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -158,7 +158,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -175,7 +175,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_coroutine_test( PRIVATE_CURTAIN_MANUAL_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, false) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_coroutine_test( -- }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -280,7 +280,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -298,7 +298,7 @@ test.register_coroutine_test( PRIVATE_CURTAIN_LOCKING_SETTING_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -316,7 +316,7 @@ test.register_coroutine_test( PRIVATE_CURTAIN_LOCKING_SETTING_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x00) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -348,7 +348,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -368,7 +368,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -388,7 +388,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -407,7 +407,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -426,7 +426,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -445,7 +445,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua index 3e1e013c38..4f8c0780c7 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_aqara_roller_shade_rotate.lua @@ -116,7 +116,7 @@ test.register_coroutine_test( PREF_REVERSE_OFF) }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -151,7 +151,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -195,7 +195,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -217,7 +217,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -246,7 +246,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -294,7 +294,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -371,7 +371,7 @@ test.register_coroutine_test( initializedStateWithGuide.initializedStateWithGuide.initialized())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -395,7 +395,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -427,7 +427,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -446,7 +446,7 @@ test.register_coroutine_test( initializedStateWithGuide.initializedStateWithGuide.notInitialized())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -466,7 +466,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -507,7 +507,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, message }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua index fb63dd121d..333eca0f9e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_axis.lua @@ -82,7 +82,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -123,7 +123,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -176,7 +176,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -204,7 +204,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -236,7 +236,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -264,7 +264,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -296,7 +296,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -336,7 +336,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -378,7 +378,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -406,7 +406,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -441,7 +441,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -469,7 +469,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -506,7 +506,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -541,7 +541,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -586,7 +586,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -624,7 +624,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -679,7 +679,7 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_device.id, WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua index 9f80bfdc80..dfecbc28ab 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_rooms.lua @@ -58,7 +58,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -80,7 +80,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -189,7 +189,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -209,7 +209,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -251,7 +251,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -296,7 +296,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.windowShadeLevel.shadeLevel(100))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -335,7 +335,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -384,7 +384,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua index a929eb4e98..17d8d61f76 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_screen_innovations.lua @@ -64,7 +64,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -86,7 +86,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -115,7 +115,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -135,7 +135,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -155,7 +155,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -175,7 +175,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -196,7 +196,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -271,7 +271,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -303,7 +303,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -346,7 +346,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -389,7 +389,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -432,7 +432,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -457,7 +457,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_sombra.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_sombra.lua index 35ab9eab24..febbfe6ea5 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_sombra.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_sombra.lua @@ -59,7 +59,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -81,7 +81,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -126,7 +126,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -171,7 +171,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -191,7 +191,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -211,7 +211,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -231,7 +231,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -253,7 +253,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -275,7 +275,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -295,7 +295,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -320,7 +320,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -343,7 +343,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -395,7 +395,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -427,7 +427,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua index e772b46bb9..7e89c956ea 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_somfy.lua @@ -68,7 +68,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -121,7 +121,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -161,7 +161,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -243,7 +243,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -267,7 +267,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -294,7 +294,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -316,7 +316,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -344,7 +344,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -372,7 +372,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -400,7 +400,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -427,7 +427,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -448,7 +448,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -475,7 +475,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -500,7 +500,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -536,7 +536,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -561,7 +561,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -591,7 +591,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -621,7 +621,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -662,7 +662,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zwave-bulb/src/test/test_aeon_multiwhite_bulb.lua b/drivers/SmartThings/zwave-bulb/src/test/test_aeon_multiwhite_bulb.lua index 8ef8ceea42..fb2b6cf81c 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_aeon_multiwhite_bulb.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_aeon_multiwhite_bulb.lua @@ -62,7 +62,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -83,7 +83,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -207,7 +207,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -232,7 +232,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -257,7 +257,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -281,7 +281,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -347,7 +347,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -379,7 +379,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -408,7 +408,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -438,7 +438,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -465,7 +465,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -493,7 +493,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -570,7 +570,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-bulb/src/test/test_aeotec_led_bulb_6.lua b/drivers/SmartThings/zwave-bulb/src/test/test_aeotec_led_bulb_6.lua index faa5125ebe..49ae2a0145 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_aeotec_led_bulb_6.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_aeotec_led_bulb_6.lua @@ -105,7 +105,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-bulb/src/test/test_fibaro_rgbw_controller.lua b/drivers/SmartThings/zwave-bulb/src/test/test_fibaro_rgbw_controller.lua index 0fc5140c4e..6d5782c676 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_fibaro_rgbw_controller.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_fibaro_rgbw_controller.lua @@ -99,7 +99,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -265,7 +265,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -297,7 +297,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -321,7 +321,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -349,7 +349,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -398,7 +398,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -478,7 +478,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -509,7 +509,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -540,7 +540,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua b/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua index a658806774..eea8a679a2 100644 --- a/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua +++ b/drivers/SmartThings/zwave-bulb/src/test/test_zwave_bulb.lua @@ -100,7 +100,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -161,7 +161,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -195,7 +195,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -228,7 +228,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -263,7 +263,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -315,7 +315,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -386,7 +386,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -431,7 +431,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -479,7 +479,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) end diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua index 7df9c6cada..a7fd84e0e6 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_aeotec_nanomote_one.lua @@ -58,7 +58,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua index 664d048010..155157e51b 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_button.lua @@ -49,7 +49,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -89,7 +89,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_coroutine_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua index 5418917ee5..39223ad82e 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_fibaro_button.lua @@ -57,7 +57,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua b/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua index b88a85a800..5e1c27b371 100644 --- a/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua +++ b/drivers/SmartThings/zwave-button/src/test/test_zwave_multi_button.lua @@ -108,7 +108,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -137,7 +137,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_coroutine_test( capabilities.button.button.double({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_coroutine_test( capabilities.button.button.double({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -276,7 +276,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -305,7 +305,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -334,7 +334,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -359,7 +359,7 @@ test.register_coroutine_test( capabilities.button.button.held({state_change = true}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -378,7 +378,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -408,7 +408,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -437,7 +437,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -466,7 +466,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -495,7 +495,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -520,7 +520,7 @@ test.register_coroutine_test( mock_aeotec_keyfob_button:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -608,7 +608,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -649,7 +649,7 @@ test.register_coroutine_test( mock_fibaro_keyfob_button:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -765,7 +765,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -853,7 +853,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -913,7 +913,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -942,7 +942,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeon_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeon_meter.lua index 391b3b2b0d..ed4d4f3609 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeon_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeon_meter.lua @@ -54,7 +54,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_coroutine_test( mock_meter:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_gen5_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_gen5_meter.lua index 016aff4061..9534c5d2e1 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_gen5_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_aeotec_gen5_meter.lua @@ -54,7 +54,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -143,7 +143,7 @@ test.register_coroutine_test( mock_meter:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_3_phase_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_3_phase_meter.lua index ce9b61f949..fad1510245 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_3_phase_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_3_phase_meter.lua @@ -96,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -180,7 +180,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -222,7 +222,7 @@ test.register_coroutine_test( mock_meter:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_smart_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_smart_meter.lua index d96d90aae1..e97be3bde3 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_smart_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_qubino_smart_meter.lua @@ -54,7 +54,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_coroutine_test( mock_meter:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-electric-meter/src/test/test_zwave_electric_meter.lua b/drivers/SmartThings/zwave-electric-meter/src/test/test_zwave_electric_meter.lua index 4df103e086..436862230d 100644 --- a/drivers/SmartThings/zwave-electric-meter/src/test/test_zwave_electric_meter.lua +++ b/drivers/SmartThings/zwave-electric-meter/src/test/test_zwave_electric_meter.lua @@ -47,7 +47,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -69,7 +69,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua index 534c48698e..179baed5aa 100644 --- a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua +++ b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_3_speed.lua @@ -64,7 +64,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -160,7 +160,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -194,7 +194,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua index 80ccef948b..fc8a5b742c 100644 --- a/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua +++ b/drivers/SmartThings/zwave-fan/src/test/test_zwave_fan_4_speed.lua @@ -63,7 +63,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -94,7 +94,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -158,7 +158,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua index 0608ca2ef2..ae86832e4d 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_ecolink_garage_door_operator.lua @@ -59,7 +59,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -78,7 +78,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_message_test( { test_init = test_init, inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -160,7 +160,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -206,7 +206,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_coroutine_test( mock_garage_door:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -264,7 +264,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -294,7 +294,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -311,7 +311,7 @@ test.register_coroutine_test( test.mock_time.advance_time(1) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -328,7 +328,7 @@ test.register_coroutine_test( test.mock_time.advance_time(1) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua index cc33391a4c..946978e0b0 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_mimolite_garage_door.lua @@ -59,7 +59,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -83,7 +83,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -179,7 +179,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -277,7 +277,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -307,7 +307,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -327,7 +327,7 @@ test.register_coroutine_test( mock_garage_door:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -350,7 +350,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua index 0d5da468e8..366824a8d2 100644 --- a/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua +++ b/drivers/SmartThings/zwave-garage-door-opener/src/test/test_zwave_garage_door_opener.lua @@ -50,7 +50,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -69,7 +69,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -93,7 +93,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -156,7 +156,7 @@ test.register_message_test( }, { test_init = test_init, - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ test.register_message_test( { test_init = test_init, inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua index 6724094462..2b9d2d3d72 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock.lua @@ -89,6 +89,9 @@ test.register_coroutine_test( test.socket.zwave:__queue_receive({ mock_device.id, Notification:Report({notification_type = 6, event = 25}) } ) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked({data={method="manual"}}))) end -) +, + { + min_api_version = 17 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua index a5c7aee7d4..63a8729ce9 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_keywe_lock_legacy.lua @@ -46,7 +46,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -59,7 +59,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua index d9642851e1..3254edf58a 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_battery.lua @@ -60,7 +60,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.unlocked())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -79,7 +79,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_coroutine_test( test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device.id)) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_coroutine_test( }):build_test_tx(mock_device.id)) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_coroutine_test( }):build_test_tx(mock_device.id)) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua index 96b192a03d..5daee2191a 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_credentials_commands.lua @@ -175,7 +175,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -200,7 +203,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -229,7 +235,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) -- ============================================================================ @@ -253,7 +262,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -275,7 +287,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -297,7 +312,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) -- ============================================================================ @@ -321,7 +339,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -341,7 +362,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -363,7 +387,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -385,7 +412,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) -- ============================================================================ @@ -423,7 +453,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -445,7 +478,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.register_coroutine_test( @@ -474,7 +510,10 @@ test.register_coroutine_test( )) ) test.wait_for_events() - end + end, + { + min_api_version = 15 + } ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_pre_configured.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_pre_configured.lua index 0895ff3215..ef264e0c88 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_pre_configured.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_pre_configured.lua @@ -205,7 +205,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- updateUser — pre-configured device @@ -240,7 +243,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "updateUser (pre-configured): returns failure for a userIndex that does not exist", @@ -273,7 +279,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- deleteUser with associated credential — exercises clear_pin_code_response @@ -324,7 +333,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- addCredential response states — set_pin_code_response @@ -365,7 +377,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "addCredential (pre-configured): emits duplicate when the lock rejects with DUPLICATE_CODE", @@ -392,7 +407,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "addCredential (pre-configured): emits failure when the lock returns available", @@ -419,7 +437,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- updateCredential response states — set_pin_code_response @@ -450,7 +471,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "updateCredential (pre-configured): returns failure immediately for a non-existent credentialIndex", @@ -472,7 +496,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- deleteCredential standalone (lockCredentials.DELETE path) @@ -509,6 +536,9 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua b/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua index da39c8c692..21c829c497 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_lock_users_commands.lua @@ -192,7 +192,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "addUser: assigns the next sequential userIndex when users already exist", @@ -223,7 +226,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "addUser: fills a gap left by a deleted user rather than appending beyond max", @@ -259,7 +265,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "addUser: returns resourceExhausted when totalUsersSupported has been reached", @@ -284,7 +293,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- updateUser @@ -322,7 +334,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "updateUser: creates user when userIndex does not exist", @@ -352,7 +367,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "updateUser: can change a user's type as well as name", @@ -380,7 +398,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- deleteUser (no associated credential — pure local delete) @@ -415,7 +436,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "deleteUser: returns failure when the target userIndex is not in the users table", @@ -435,7 +459,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- deleteAllUsers (injects deleteAllCredentials → ClearAllPINCodes z-wave flow) @@ -495,7 +522,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "deleteAllUsers: emits busy when another lock command is in progress", @@ -517,7 +547,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- State-consistency: add → delete → re-add (indices 1, 2, 3 lifecycle) @@ -590,7 +623,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "State-consistency: add users+credentials 1-3, deleteUser 2 (with credential), re-add user+credential reclaims index 2 cleanly", @@ -700,6 +736,9 @@ test.register_coroutine_test( }) == constants.COMMAND_RESULT.SUCCESS) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua index 81ba1df2ad..42cb17a2a2 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_samsung_lock_legacy.lua @@ -51,7 +51,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged("2 failed", { state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lockCodes.codeChanged(2 .. " failed", { state_change = true }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua index 38dabbd9dd..3c8a165f7f 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_schlage_lock_legacy.lua @@ -77,7 +77,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -232,7 +232,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -259,7 +259,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua index 66038d0760..b7ddf56227 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_lock_legacy.lua @@ -62,7 +62,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.lock.lock.locked())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -137,7 +137,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -179,7 +179,7 @@ test.register_coroutine_test( test.socket.zwave:__expect_send(DoorLock:OperationGet({}):build_test_tx(mock_device.id)) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -209,7 +209,7 @@ test.register_coroutine_test( expect_reload_all_codes_messages() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -228,7 +228,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_coroutine_test( test.socket.zwave:__expect_send(UserCode:Get( {user_identifier = 1}):build_test_tx(mock_device.id)) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -264,7 +264,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -289,7 +289,7 @@ test.register_coroutine_test( {state_change = true}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -316,7 +316,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -341,7 +341,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -371,7 +371,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -395,7 +395,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -438,7 +438,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -463,7 +463,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -488,7 +488,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -519,7 +519,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -754,7 +754,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -779,7 +779,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -808,7 +808,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-lock/src/test/test_zwave_responses.lua b/drivers/SmartThings/zwave-lock/src/test/test_zwave_responses.lua index 71bf25209d..24db26a3ba 100644 --- a/drivers/SmartThings/zwave-lock/src/test/test_zwave_responses.lua +++ b/drivers/SmartThings/zwave-lock/src/test/test_zwave_responses.lua @@ -100,7 +100,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "user_code_report: sync completes when the last slot is reached", @@ -140,7 +143,10 @@ test.register_coroutine_test( assert(mock_device:get_field(constants.DRIVER_STATE.BUSY) == false, "BUSY must be false after sync completes") end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "user_code_report: out-of-band ENABLED code creates user and credential entries", @@ -173,7 +179,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- user_code_event_handler (ACCESS_CONTROL Notification:Report) @@ -208,7 +217,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "user_code_event: multi-byte event_parameter uses byte 3 to extract user_id", @@ -239,7 +251,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- door_operation_event_handler (ACCESS_CONTROL Notification:Report) @@ -279,7 +294,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "door_operation_event: KEYPAD_UNLOCK_OPERATION with unknown credential sets userIndex from event_parameter", @@ -306,7 +324,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "door_operation_event: AUTO_LOCK_LOCKED_OPERATION emits locked with auto method", @@ -327,7 +348,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- DoorLock:OperationReport → lock state @@ -346,7 +370,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.register_coroutine_test( "OperationReport: DOOR_UNSECURED emits unlocked event", @@ -360,7 +387,10 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) -- ============================================================================ -- users_number_report: UserCode:UsersNumberReport @@ -388,6 +418,9 @@ test.register_coroutine_test( ) test.wait_for_events() end -) +, + { + min_api_version = 15 + }) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-mouse-trap/src/test/test_zwave_mouse_trap.lua b/drivers/SmartThings/zwave-mouse-trap/src/test/test_zwave_mouse_trap.lua index 519f577c86..ca1ae9ede4 100644 --- a/drivers/SmartThings/zwave-mouse-trap/src/test/test_zwave_mouse_trap.lua +++ b/drivers/SmartThings/zwave-mouse-trap/src/test/test_zwave_mouse_trap.lua @@ -67,7 +67,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -89,7 +89,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -111,7 +111,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -262,7 +262,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -281,7 +281,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -335,7 +335,7 @@ test.register_coroutine_test( mock_mouse_trap:expect_metadata_update({provisioning_state = "PROVISIONED"}) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua index 472932fdfb..649855f9c3 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeon_multisensor.lua @@ -83,7 +83,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua index 323c0d4807..9e7a02851b 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_6.lua @@ -108,7 +108,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -231,7 +231,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -288,7 +288,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -323,7 +323,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -345,7 +345,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -369,7 +369,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -405,7 +405,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -429,7 +429,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -454,7 +454,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_7.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_7.lua index 13442ec635..648fb9a5ad 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_7.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_7.lua @@ -110,7 +110,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -229,7 +229,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -251,7 +251,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -286,7 +286,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -321,7 +321,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_gen5.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_gen5.lua index b6ede32bd5..25bba0e7b8 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_gen5.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_multisensor_gen5.lua @@ -98,7 +98,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor.lua index e38ce96f37..62ab74cfde 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor.lua @@ -78,7 +78,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -160,7 +160,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -182,7 +182,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -223,7 +223,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -245,7 +245,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -267,7 +267,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -289,7 +289,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -311,7 +311,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor_7.lua b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor_7.lua index 0f3dc972de..26e61fae14 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor_7.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_aeotec_water_sensor_7.lua @@ -52,7 +52,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -74,7 +74,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -140,7 +140,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_sensor:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_enerwave_motion_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_enerwave_motion_sensor.lua index 3559656e3a..73af4449dc 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_enerwave_motion_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_enerwave_motion_sensor.lua @@ -53,7 +53,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua index a33f9b97ec..df57046912 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everpsring_sp817.lua @@ -51,7 +51,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -88,7 +88,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_PIR_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_PIR_sensor.lua index 8479a5f74f..9efe546309 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_PIR_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_PIR_sensor.lua @@ -58,7 +58,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_sensor:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -130,7 +130,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -153,7 +153,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua index 3c288c0be4..1bef4fb686 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_ST814.lua @@ -50,7 +50,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_illuminance_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_illuminance_sensor.lua index ab124e8f80..dfce8285d9 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_illuminance_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_illuminance_sensor.lua @@ -54,7 +54,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_motion_light_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_motion_light_sensor.lua index 0a31f97de5..0babb02dc1 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_everspring_motion_light_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_everspring_motion_light_sensor.lua @@ -66,7 +66,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_ezmultipli_multipurpose_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_ezmultipli_multipurpose_sensor.lua index fb2138c0d5..08289dd301 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_ezmultipli_multipurpose_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_ezmultipli_multipurpose_sensor.lua @@ -52,7 +52,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ do )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ do )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -293,7 +293,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor.lua index 05e306b6c7..386891a9cc 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor.lua @@ -73,7 +73,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -97,7 +97,7 @@ test.register_coroutine_test( mock_fibaro_door_window_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -160,7 +160,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua index 6707dd7ae0..8e9c936237 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_1.lua @@ -121,7 +121,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -210,7 +210,7 @@ test.register_coroutine_test( mock_fibaro_door_window_sensor1:expect_metadata_update({provisioning_state = "PROVISIONED"}) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -251,7 +251,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -273,7 +273,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -300,7 +300,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -327,7 +327,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -349,7 +349,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -370,7 +370,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -401,7 +401,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -431,7 +431,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua index 8fd23b208b..7a1ddb417e 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_2.lua @@ -59,7 +59,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -103,7 +103,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -147,7 +147,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -191,7 +191,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -308,7 +308,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -349,7 +349,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -385,7 +385,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua index 4d3e6e0660..0b970eded6 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_door_window_sensor_with_temperature.lua @@ -85,7 +85,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_coroutine_test( mock_fibaro_door_window_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -132,7 +132,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -154,7 +154,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -252,7 +252,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -282,7 +282,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -559,7 +559,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua index 0ac7e57418..7de8a345b3 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor.lua @@ -54,7 +54,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -94,7 +94,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -115,7 +115,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -167,7 +167,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -189,7 +189,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_sensor:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_sensor:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -270,7 +270,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_sensor:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -297,7 +297,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_sensor:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -328,7 +328,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor_zw5.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor_zw5.lua index 530d1c03bb..dc47194f8c 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor_zw5.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_flood_sensor_zw5.lua @@ -82,7 +82,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua index 3dbfd89bc7..3a2f5b3644 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor.lua @@ -87,7 +87,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -106,7 +106,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -266,7 +266,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -288,7 +288,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -315,7 +315,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -346,7 +346,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -376,7 +376,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor_zw5.lua b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor_zw5.lua index ac2022e069..22f9df61db 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor_zw5.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_fibaro_motion_sensor_zw5.lua @@ -69,7 +69,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua b/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua index bf4c68d5bb..e85f290f11 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_firmware_version.lua @@ -86,7 +86,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua index 3496ebb6d5..9f8019e240 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_generic_sensor.lua @@ -63,7 +63,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -131,7 +131,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -154,7 +154,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -207,7 +207,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -238,7 +238,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -260,7 +260,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -283,7 +283,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -344,7 +344,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -366,7 +366,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -388,7 +388,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -412,7 +412,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -456,7 +456,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -500,7 +500,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -522,7 +522,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -544,7 +544,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -571,7 +571,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -598,7 +598,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -692,7 +692,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -786,7 +786,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -808,7 +808,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -830,7 +830,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -852,7 +852,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -874,7 +874,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -896,7 +896,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -918,7 +918,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -940,7 +940,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -962,7 +962,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -984,7 +984,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1006,7 +1006,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1028,7 +1028,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1050,7 +1050,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1072,7 +1072,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1105,7 +1105,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1127,7 +1127,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1149,7 +1149,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1171,7 +1171,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1199,7 +1199,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1221,7 +1221,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1243,7 +1243,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1265,7 +1265,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1287,7 +1287,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1309,7 +1309,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1331,7 +1331,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1353,7 +1353,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1375,7 +1375,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1397,7 +1397,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1419,7 +1419,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1441,7 +1441,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1463,7 +1463,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1485,7 +1485,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1506,7 +1506,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1527,7 +1527,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1549,7 +1549,7 @@ test.register_coroutine_test( test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command(mock_device, Battery:Get({}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1571,7 +1571,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1593,7 +1593,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_glentronics_water_leak_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_glentronics_water_leak_sensor.lua index 926558036b..52a3a50b9a 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_glentronics_water_leak_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_glentronics_water_leak_sensor.lua @@ -70,7 +70,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -196,7 +196,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -222,7 +222,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_homeseer_multi_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_homeseer_multi_sensor.lua index 743ffc5298..dd9101edf7 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_homeseer_multi_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_homeseer_multi_sensor.lua @@ -51,7 +51,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -70,7 +70,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -92,7 +92,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -137,7 +137,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -168,7 +168,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) test.run_registered_tests() diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_no_wakeup_poll.lua b/drivers/SmartThings/zwave-sensor/src/test/test_no_wakeup_poll.lua index c88527f0cd..c9eb716c3b 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_no_wakeup_poll.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_no_wakeup_poll.lua @@ -100,7 +100,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua b/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua index fb9b519a42..0071f3f09a 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_sensative_strip.lua @@ -61,7 +61,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -72,7 +72,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ profile = "illuminance-temperature" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -94,7 +94,7 @@ test.register_coroutine_test( test.socket.zwave:__queue_receive({mock_sensor.id, WakeUp:Notification({})}) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua index e80e28dbd9..cf7d08ebdf 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_smartthings_water_leak_sensor.lua @@ -59,7 +59,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -103,7 +103,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -147,7 +147,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -191,7 +191,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -222,7 +222,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -253,7 +253,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -272,7 +272,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -291,7 +291,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -310,7 +310,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_v1_contact_event.lua b/drivers/SmartThings/zwave-sensor/src/test/test_v1_contact_event.lua index 4cfd906636..0db72f1b50 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_v1_contact_event.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_v1_contact_event.lua @@ -57,7 +57,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -104,7 +104,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_vision_motion_detector.lua b/drivers/SmartThings/zwave-sensor/src/test/test_vision_motion_detector.lua index 3cd16b6fa3..11d62dd11f 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_vision_motion_detector.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_vision_motion_detector.lua @@ -58,7 +58,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -80,7 +80,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua index fde2d5ebfe..63b0b2bb8a 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zooz_4_in_1_sensor.lua @@ -53,7 +53,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -103,7 +103,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_sensor:generate_test_message("main", capabilities.tamperAlert.tamper.clear())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -214,7 +214,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -239,7 +239,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -260,7 +260,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_light_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_light_sensor.lua index 7a8adbeb10..5559a53c79 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_light_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_light_sensor.lua @@ -59,7 +59,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -151,7 +151,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -196,7 +196,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -218,7 +218,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -245,7 +245,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -292,7 +292,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -307,7 +307,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_temp_light_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_temp_light_sensor.lua index 8e463e7625..b6c70bdadb 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_temp_light_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_motion_temp_light_sensor.lua @@ -55,7 +55,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -82,7 +82,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -104,7 +104,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -126,7 +126,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -192,7 +192,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua index 677204e5d1..29354370e4 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_sensor.lua @@ -131,7 +131,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -235,7 +235,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -272,7 +272,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -294,7 +294,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -316,7 +316,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -338,7 +338,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -359,7 +359,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -380,7 +380,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -411,7 +411,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -438,7 +438,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -460,7 +460,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -482,7 +482,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -504,7 +504,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -526,7 +526,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -548,7 +548,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -570,7 +570,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -602,7 +602,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -624,7 +624,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -646,7 +646,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -673,7 +673,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -700,7 +700,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -727,7 +727,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -749,7 +749,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -771,7 +771,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -794,7 +794,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -815,7 +815,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -836,7 +836,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_water_sensor.lua b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_water_sensor.lua index 775130d87e..fa5f64bff0 100644 --- a/drivers/SmartThings/zwave-sensor/src/test/test_zwave_water_sensor.lua +++ b/drivers/SmartThings/zwave-sensor/src/test/test_zwave_water_sensor.lua @@ -57,7 +57,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -161,7 +161,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -224,7 +224,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -246,7 +246,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -268,7 +268,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -290,7 +290,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua index 30b8333c5b..8e8137cc90 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_aeon_siren.lua @@ -56,7 +56,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -82,7 +82,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -207,7 +207,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -238,7 +238,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -265,7 +265,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -293,7 +293,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -322,7 +322,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_aeotec_doorbell_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_aeotec_doorbell_siren.lua index 31fb57fed2..d58fbfc207 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_aeotec_doorbell_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_aeotec_doorbell_siren.lua @@ -113,7 +113,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -151,7 +151,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -189,7 +189,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -227,7 +227,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -265,7 +265,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -303,7 +303,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -341,7 +341,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -379,7 +379,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -424,7 +424,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -469,7 +469,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -514,7 +514,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -559,7 +559,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -604,7 +604,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -649,7 +649,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -694,7 +694,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -739,7 +739,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -779,7 +779,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -819,7 +819,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -859,7 +859,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -899,7 +899,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -939,7 +939,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -979,7 +979,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1019,7 +1019,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1059,7 +1059,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1099,7 +1099,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1139,7 +1139,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1179,7 +1179,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1219,7 +1219,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1259,7 +1259,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1299,7 +1299,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1339,7 +1339,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1379,7 +1379,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1419,7 +1419,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1459,7 +1459,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1499,7 +1499,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1539,7 +1539,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1579,7 +1579,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1619,7 +1619,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1659,7 +1659,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1699,7 +1699,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1739,7 +1739,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1779,7 +1779,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1819,7 +1819,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1859,7 +1859,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1899,7 +1899,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1939,7 +1939,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1979,7 +1979,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2019,7 +2019,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2059,7 +2059,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2099,7 +2099,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2139,7 +2139,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2179,7 +2179,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2219,7 +2219,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2259,7 +2259,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2299,7 +2299,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2339,7 +2339,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2379,7 +2379,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2419,7 +2419,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2459,7 +2459,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2499,7 +2499,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2539,7 +2539,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2579,7 +2579,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2619,7 +2619,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2659,7 +2659,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2682,7 +2682,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -2705,7 +2705,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -2738,7 +2738,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2754,7 +2754,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_siren:generate_info_changed({ preferences = _preferences})) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2779,7 +2779,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2793,7 +2793,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_siren:generate_info_changed({ preferences = _preferences})) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2818,7 +2818,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2832,7 +2832,7 @@ test.register_coroutine_test( test.socket.device_lifecycle:__queue_receive(mock_siren:generate_info_changed({ preferences = _preferences})) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2860,7 +2860,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2888,7 +2888,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2916,7 +2916,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2944,7 +2944,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2972,7 +2972,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -3000,7 +3000,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -3036,7 +3036,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -3056,7 +3056,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_ecolink_wireless_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_ecolink_wireless_siren.lua index c81ba1b262..ab6fab08a9 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_ecolink_wireless_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_ecolink_wireless_siren.lua @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -106,7 +106,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -210,7 +210,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -248,7 +248,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -286,7 +286,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -313,7 +313,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -347,7 +347,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -374,7 +374,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -400,7 +400,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -427,7 +427,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -452,7 +452,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -477,7 +477,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) test.register_coroutine_test( @@ -502,7 +502,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua index 61ae247d58..90a01396c8 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_fortrezz_siren.lua @@ -61,7 +61,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_siren:generate_test_message("main", capabilities.switch.switch.on({}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_siren:generate_test_message("main", capabilities.switch.switch.on({}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -128,7 +128,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_siren:generate_test_message("main", capabilities.switch.switch.on({}))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -161,7 +161,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_siren:generate_test_message("main", capabilities.switch.switch.off({}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_philio_sound_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_philio_sound_siren.lua index 432de972dd..06255805f0 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_philio_sound_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_philio_sound_siren.lua @@ -64,7 +64,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -224,7 +224,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -281,7 +281,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -308,7 +308,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -360,7 +360,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -387,7 +387,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -414,7 +414,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -441,7 +441,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -462,7 +462,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -484,7 +484,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -533,7 +533,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -558,7 +558,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua index 33801f1dbd..0632779570 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_utilitech_siren.lua @@ -43,7 +43,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -63,7 +63,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua index 275cc89ce9..517947dfef 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_yale_siren.lua @@ -54,7 +54,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -75,7 +75,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -227,7 +227,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -263,7 +263,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -311,7 +311,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -331,7 +331,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua index 0d39f0f990..6c5672928b 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zipato_siren.lua @@ -63,7 +63,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -94,7 +94,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -135,7 +135,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -156,7 +156,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zooz_zse50.lua b/drivers/SmartThings/zwave-siren/src/test/test_zooz_zse50.lua index 1454458687..c7d5723d64 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zooz_zse50.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zooz_zse50.lua @@ -66,7 +66,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -242,7 +242,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -264,7 +264,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -296,7 +296,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -341,7 +341,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -372,7 +372,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -403,7 +403,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -434,7 +434,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -453,7 +453,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -472,7 +472,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -491,7 +491,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -516,7 +516,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -541,7 +541,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -566,7 +566,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -591,7 +591,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -616,7 +616,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -641,7 +641,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -663,7 +663,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -710,7 +710,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zwave_multifunctional-siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_zwave_multifunctional-siren.lua index 61893d7dd6..27a2833d17 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_multifunctional-siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_multifunctional-siren.lua @@ -52,7 +52,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -74,7 +74,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_coroutine_test( mock_siren:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zwave_notification_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_zwave_notification_siren.lua index e80d79e338..9fa63cfcba 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_notification_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_notification_siren.lua @@ -51,7 +51,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -78,7 +78,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua b/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua index 3e33929169..2311a1dafa 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_siren.lua @@ -84,7 +84,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -214,7 +214,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -240,7 +240,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -266,7 +266,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -297,7 +297,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -328,7 +328,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -360,7 +360,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -391,7 +391,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -422,7 +422,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -463,7 +463,7 @@ test.register_coroutine_test( test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-siren/src/test/test_zwave_sound_sensor.lua b/drivers/SmartThings/zwave-siren/src/test/test_zwave_sound_sensor.lua index b796b1b9d0..716c05ac0d 100644 --- a/drivers/SmartThings/zwave-siren/src/test/test_zwave_sound_sensor.lua +++ b/drivers/SmartThings/zwave-siren/src/test/test_zwave_sound_sensor.lua @@ -50,7 +50,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -72,7 +72,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -91,7 +91,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua index e860e4608f..7031ffb88b 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_co_sensor_zw5.lua @@ -70,7 +70,7 @@ test.register_coroutine_test( mock_fibaro_CO_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -89,7 +89,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -226,7 +226,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -279,7 +279,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -307,7 +307,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -334,7 +334,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -361,7 +361,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -392,7 +392,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -501,7 +501,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua index 39c79e0498..c877d18e47 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_fibaro_smoke_sensor.lua @@ -150,7 +150,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -209,7 +209,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua index bdeb10535f..af2588b959 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_alarm_v1.lua @@ -54,7 +54,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -98,7 +98,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -165,7 +165,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -209,7 +209,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua index 60710942bc..3bf9d563a1 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_co_detector.lua @@ -55,7 +55,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -77,7 +77,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) test.register_message_test( @@ -98,7 +98,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -164,7 +164,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -181,7 +181,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua index c62dab7ec6..838527222b 100644 --- a/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua +++ b/drivers/SmartThings/zwave-smoke-alarm/src/test/test_zwave_smoke_detector.lua @@ -56,7 +56,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -78,7 +78,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -122,7 +122,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -166,7 +166,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -205,7 +205,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -297,7 +297,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -319,7 +319,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -341,7 +341,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -363,7 +363,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -407,7 +407,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeon_smart_strip.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeon_smart_strip.lua index d0db48c559..86b5d5c539 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeon_smart_strip.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeon_smart_strip.lua @@ -152,7 +152,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -241,7 +241,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -292,7 +292,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -343,7 +343,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -377,7 +377,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -411,7 +411,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -445,7 +445,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -515,7 +515,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -549,7 +549,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -621,7 +621,7 @@ test.register_coroutine_test( mock_metering_switch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -755,7 +755,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -889,7 +889,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -980,7 +980,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1071,7 +1071,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1162,7 +1162,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1253,7 +1253,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dimmer_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dimmer_switch.lua index ad9197c159..6829cd1c1b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dimmer_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dimmer_switch.lua @@ -60,7 +60,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -139,7 +139,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -179,7 +179,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -223,7 +223,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -280,7 +280,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -312,7 +312,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -336,7 +336,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -405,7 +405,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -475,7 +475,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -510,7 +510,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dual_nano_switch_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dual_nano_switch_configuration.lua index 6f35d6200c..d951926231 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dual_nano_switch_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_dual_nano_switch_configuration.lua @@ -113,7 +113,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_heavy_duty_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_heavy_duty_switch.lua index 2d9b6e24c5..73228366f8 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_heavy_duty_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_heavy_duty_switch.lua @@ -57,7 +57,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_coroutine_test( Meter:Get({ scale = Meter.scale.electric_meter.KILOWATT_HOURS }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -246,7 +246,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -273,7 +273,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -300,7 +300,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -327,7 +327,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -354,7 +354,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -381,7 +381,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -408,7 +408,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -435,7 +435,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -462,7 +462,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -489,7 +489,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -516,7 +516,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_metering_switch_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_metering_switch_configuration.lua index c26c0403e4..eb022e95b5 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_metering_switch_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_metering_switch_configuration.lua @@ -65,7 +65,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer.lua index 2c009b5ec7..7c86c0c830 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer.lua @@ -84,7 +84,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -304,7 +304,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -336,7 +336,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -360,7 +360,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -429,7 +429,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -499,7 +499,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -534,7 +534,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer_preferences.lua index 5a349d80cb..2022dbf055 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_nano_dimmer_preferences.lua @@ -59,7 +59,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch.lua index c61e1835a8..507aa2484f 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch.lua @@ -60,7 +60,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -90,7 +90,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_eu.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_eu.lua index fcaed8070a..4cbfdea3a6 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_eu.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_eu.lua @@ -66,7 +66,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -175,7 +175,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -222,7 +222,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_us.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_us.lua index 51f7b504ca..47a222e06e 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_us.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_7_us.lua @@ -54,7 +54,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_gen5.lua b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_gen5.lua index 565d6b57aa..4effe6c50a 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_gen5.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_aeotec_smart_switch_gen5.lua @@ -55,7 +55,7 @@ test.register_coroutine_test( mock_switch:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_dawon_smart_plug.lua b/drivers/SmartThings/zwave-switch/src/test/test_dawon_smart_plug.lua index 6615af4a12..5270baa847 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_dawon_smart_plug.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_dawon_smart_plug.lua @@ -49,7 +49,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -71,7 +71,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_dawon_wall_smart_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_dawon_wall_smart_switch.lua index 3fe41b79db..1fe37d24ca 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_dawon_wall_smart_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_dawon_wall_smart_switch.lua @@ -77,7 +77,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -106,7 +106,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -266,7 +266,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -298,7 +298,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -321,7 +321,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -385,7 +385,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_eaton_5_scene_keypad.lua b/drivers/SmartThings/zwave-switch/src/test/test_eaton_5_scene_keypad.lua index 29ff5d699e..9cbfd0b38b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_eaton_5_scene_keypad.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_eaton_5_scene_keypad.lua @@ -76,7 +76,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -147,7 +147,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -235,7 +235,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -273,7 +273,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -381,7 +381,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -398,7 +398,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -458,7 +458,7 @@ test.register_coroutine_test( -- if group_id and scene_id are same, do nothing. end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -483,7 +483,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -501,7 +501,7 @@ test.register_coroutine_test( -- driver should do nothing. end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -521,7 +521,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -541,7 +541,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_eaton_accessory_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_eaton_accessory_dimmer.lua index 103f856373..863f6b5d91 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_eaton_accessory_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_eaton_accessory_dimmer.lua @@ -44,7 +44,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -95,7 +95,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -130,7 +130,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) end @@ -203,7 +203,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -257,7 +257,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_eaton_anyplace_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_eaton_anyplace_switch.lua index 3c1fdfc9eb..79f6390aea 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_eaton_anyplace_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_eaton_anyplace_switch.lua @@ -73,7 +73,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -119,7 +119,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -165,7 +165,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -184,7 +184,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_eaton_rf_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_eaton_rf_dimmer.lua index 10a784d8b4..5b1623758c 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_eaton_rf_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_eaton_rf_dimmer.lua @@ -43,7 +43,7 @@ test.register_coroutine_test( mock_sensor:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua index 51ad9b8299..e5ccfc170a 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_ecolink_switch.lua @@ -63,7 +63,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -94,7 +94,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -217,7 +217,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -249,7 +249,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -274,7 +274,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -299,7 +299,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_double_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_double_switch.lua index 29e6b9566e..81705116e3 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_double_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_double_switch.lua @@ -135,7 +135,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) test.register_message_test( @@ -237,7 +237,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -288,7 +288,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -328,7 +328,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -368,7 +368,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -408,7 +408,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -448,7 +448,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -601,7 +601,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -653,7 +653,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_single_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_single_switch.lua index 4c8d058acf..3792254e03 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_single_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_single_switch.lua @@ -55,7 +55,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -109,7 +109,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -175,7 +175,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -238,7 +238,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -267,7 +267,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -290,7 +290,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -319,7 +319,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -342,7 +342,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -371,7 +371,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -394,7 +394,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -423,7 +423,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -446,7 +446,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -475,7 +475,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -497,7 +497,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -526,7 +526,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -548,7 +548,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -577,7 +577,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -691,7 +691,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_eu.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_eu.lua index 669b01f1bd..f35d61d836 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_eu.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_eu.lua @@ -108,7 +108,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_uk_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_uk_configuration.lua index a051ce72b2..c6ab67534f 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_uk_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_uk_configuration.lua @@ -57,7 +57,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_us.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_us.lua index 2fafc237b2..0a49231bc0 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_us.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_wall_plug_us.lua @@ -71,7 +71,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -108,7 +108,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -210,7 +210,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -258,7 +258,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -340,7 +340,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_dimmer_preferences.lua index 3dd35e66b9..cae11364c1 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_dimmer_preferences.lua @@ -53,7 +53,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -77,7 +77,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -149,7 +149,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -174,7 +174,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -198,7 +198,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch.lua index 2ced0ef6a3..93d19204f4 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch.lua @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -173,7 +173,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -254,7 +254,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -310,7 +310,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -366,7 +366,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -413,7 +413,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -452,7 +452,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -522,7 +522,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -591,7 +591,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -658,7 +658,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -727,7 +727,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -771,7 +771,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -806,7 +806,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -841,7 +841,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch_preferences.lua index 1e2cfe45aa..9001400001 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_fibaro_walli_double_switch_preferences.lua @@ -53,7 +53,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -93,7 +93,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -153,7 +153,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_generic_zwave_device1.lua b/drivers/SmartThings/zwave-switch/src/test/test_generic_zwave_device1.lua index 5116eceada..a5865cd219 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_generic_zwave_device1.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_generic_zwave_device1.lua @@ -45,7 +45,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -71,7 +71,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -245,7 +245,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_go_control_plug_in_switch_configuraton.lua b/drivers/SmartThings/zwave-switch/src/test/test_go_control_plug_in_switch_configuraton.lua index 4827c600d8..a83e2eddc1 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_go_control_plug_in_switch_configuraton.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_go_control_plug_in_switch_configuraton.lua @@ -49,7 +49,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua index 66cbf38817..ed0c1bfed1 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_honeywell_dimmer.lua @@ -55,7 +55,7 @@ test.register_coroutine_test( mock_dimmer:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_2_channel_smart_plug.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_2_channel_smart_plug.lua index 65ccf723b3..32de5e2715 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_2_channel_smart_plug.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_2_channel_smart_plug.lua @@ -97,7 +97,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -161,7 +161,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -290,7 +290,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -405,7 +405,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -478,7 +478,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -538,7 +538,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -598,7 +598,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -662,7 +662,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -726,7 +726,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -774,7 +774,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -822,7 +822,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -862,7 +862,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -903,7 +903,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -943,7 +943,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -984,7 +984,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua index 96668cfc24..8bf0b6f6f9 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_button.lua @@ -131,7 +131,7 @@ test.register_coroutine_test( end end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua index 0e71010aa3..bb62be473b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer.lua @@ -47,7 +47,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -73,7 +73,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -200,7 +200,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -233,7 +233,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) end diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_led.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_led.lua index 48fde4f573..59588c0e52 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_led.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_led.lua @@ -99,7 +99,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -135,7 +135,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_power_energy.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_power_energy.lua index 4a3ab7f64a..dfdaf4ac54 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_power_energy.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_power_energy.lua @@ -70,7 +70,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -200,7 +200,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -276,7 +276,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -307,7 +307,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -340,7 +340,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_preferences.lua index 701fd6513a..a0c83ed6ae 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_preferences.lua @@ -56,7 +56,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -84,7 +84,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -140,7 +140,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -169,7 +169,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua index 81e4eff5c0..aa89df6bf2 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_dimmer_scenes.lua @@ -59,7 +59,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -82,7 +82,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -128,7 +128,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -151,7 +151,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -174,7 +174,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -266,7 +266,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -289,7 +289,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua index 5a9b2ccafd..cec88fe366 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn.lua @@ -130,7 +130,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -188,7 +188,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -222,7 +222,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -248,7 +248,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -274,7 +274,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -333,7 +333,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua index 4fcdf9e65c..243bd08640 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_child.lua @@ -90,7 +90,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -179,7 +179,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -238,7 +238,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -344,7 +344,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua index 3c3e9aa7b8..be6db7e82b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_inovelli_vzw32_sn_preferences.lua @@ -60,7 +60,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ do }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_multi_metering_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_multi_metering_switch.lua index ad95d3edbb..3586b93aab 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_multi_metering_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_multi_metering_switch.lua @@ -116,7 +116,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -168,7 +168,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -248,7 +248,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -276,7 +276,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -304,7 +304,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -332,7 +332,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -381,7 +381,7 @@ test.register_coroutine_test( mock_parent_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -424,7 +424,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -467,7 +467,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -497,7 +497,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -531,7 +531,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -563,7 +563,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -595,7 +595,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -630,7 +630,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -663,7 +663,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -757,7 +757,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -774,7 +774,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua b/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua index 6c835ff893..c16703f52b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_multichannel_device.lua @@ -169,7 +169,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -206,7 +206,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -243,7 +243,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -280,7 +280,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -330,7 +330,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -375,7 +375,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -420,7 +420,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -470,7 +470,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -520,7 +520,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -565,7 +565,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -610,7 +610,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -660,7 +660,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -703,7 +703,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -746,7 +746,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -789,7 +789,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -832,7 +832,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -888,7 +888,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -931,7 +931,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -987,7 +987,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1057,7 +1057,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1099,7 +1099,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1141,7 +1141,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1183,7 +1183,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1229,7 +1229,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1283,7 +1283,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1312,7 +1312,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1341,7 +1341,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1375,7 +1375,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1409,7 +1409,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1533,7 +1533,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1657,7 +1657,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1786,7 +1786,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1815,7 +1815,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1844,7 +1844,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1873,7 +1873,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1902,7 +1902,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1931,7 +1931,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1960,7 +1960,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1989,7 +1989,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2018,7 +2018,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2047,7 +2047,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2076,7 +2076,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2105,7 +2105,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2134,7 +2134,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2172,7 +2172,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -2201,7 +2201,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2231,7 +2231,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2281,7 +2281,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -2337,7 +2337,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -2465,7 +2465,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -2509,7 +2509,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -2531,7 +2531,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2550,7 +2550,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2572,7 +2572,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2591,7 +2591,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2613,7 +2613,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2632,7 +2632,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2654,7 +2654,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -2673,7 +2673,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_popp_outdoor_plug_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_popp_outdoor_plug_configuration.lua index 8586267722..b972f5c936 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_popp_outdoor_plug_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_popp_outdoor_plug_configuration.lua @@ -49,7 +49,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer.lua index 151b6b416e..8688cd7ec4 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer.lua @@ -70,7 +70,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -125,7 +125,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -165,7 +165,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -206,7 +206,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -307,7 +307,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -339,7 +339,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -363,7 +363,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -382,7 +382,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -451,7 +451,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -521,7 +521,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -555,7 +555,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer_preferences.lua index 3a2ef76aa2..86e3f8460b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_din_dimmer_preferences.lua @@ -56,7 +56,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -80,7 +80,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -104,7 +104,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -128,7 +128,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -177,7 +177,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1_relay_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1_relay_preferences.lua index 959bb2068d..2c1d6f337b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1_relay_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1_relay_preferences.lua @@ -48,7 +48,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -72,7 +72,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1d_relay_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1d_relay_preferences.lua index 89b03dae43..f67ac02a67 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1d_relay_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_1d_relay_preferences.lua @@ -48,7 +48,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -72,7 +72,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay.lua index 53e8744577..646370e760 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay.lua @@ -106,7 +106,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -138,7 +138,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -187,7 +187,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -215,7 +215,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -240,7 +240,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -265,7 +265,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -292,7 +292,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -319,7 +319,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -354,7 +354,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -389,7 +389,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -418,7 +418,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -460,7 +460,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -502,7 +502,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -544,7 +544,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -586,7 +586,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -639,7 +639,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay_preferences.lua index b894310832..e050946859 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_2_relay_preferences.lua @@ -48,7 +48,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -72,7 +72,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -96,7 +96,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -144,7 +144,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer.lua index 42e01fe944..7352331b05 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer.lua @@ -61,7 +61,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -156,7 +156,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -197,7 +197,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -241,7 +241,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -298,7 +298,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -330,7 +330,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -354,7 +354,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -371,7 +371,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 21.5, unit = 'C' }))) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -440,7 +440,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -510,7 +510,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -544,7 +544,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_0_10V_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_0_10V_preferences.lua index 7256e3579a..7dad04cd8c 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_0_10V_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_0_10V_preferences.lua @@ -52,7 +52,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -100,7 +100,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -148,7 +148,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -176,7 +176,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_preferences.lua index 347995922e..95c994a637 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_flush_dimmer_preferences.lua @@ -51,7 +51,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -75,7 +75,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -147,7 +147,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -171,7 +171,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -195,7 +195,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -244,7 +244,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_mini_dimmer_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_mini_dimmer_preferences.lua index c1c4f31f0b..d510e9cfed 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_mini_dimmer_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_mini_dimmer_preferences.lua @@ -51,7 +51,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -75,7 +75,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -147,7 +147,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -196,7 +196,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -220,7 +220,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_with_power.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_with_power.lua index 2614927458..0606989b4d 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_with_power.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_with_power.lua @@ -59,7 +59,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -140,7 +140,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -196,7 +196,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -290,7 +290,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -360,7 +360,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -404,7 +404,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_without_power.lua b/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_without_power.lua index c13617986b..7a2c7ea56b 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_without_power.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_qubino_temperature_sensor_without_power.lua @@ -60,7 +60,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -92,7 +92,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -117,7 +117,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_coroutine_test( mock_device:expect_native_attr_handler_registration("switch", "switch") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -224,7 +224,7 @@ test.register_coroutine_test( mock_device:expect_native_attr_handler_registration("switch", "switch") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -268,7 +268,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_shelly_multi_metering_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_shelly_multi_metering_switch.lua index 0d3759c314..28165e645c 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_shelly_multi_metering_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_shelly_multi_metering_switch.lua @@ -115,7 +115,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -167,7 +167,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -247,7 +247,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -275,7 +275,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -303,7 +303,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -331,7 +331,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -374,7 +374,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -417,7 +417,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -447,7 +447,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -481,7 +481,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -513,7 +513,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -545,7 +545,7 @@ do } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -580,7 +580,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -613,7 +613,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -707,7 +707,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -724,7 +724,7 @@ test.register_coroutine_test( }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua index c7288aafaf..a09be9a417 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch.lua @@ -71,7 +71,7 @@ test.register_coroutine_test( mock_parent_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -99,7 +99,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -127,7 +127,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -183,7 +183,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -249,7 +249,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -283,7 +283,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -316,7 +316,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -350,7 +350,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -369,7 +369,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch_configuration.lua b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch_configuration.lua index fbf70da972..fcd724acab 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch_configuration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_wyfy_touch_configuration.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zooz_double_plug.lua b/drivers/SmartThings/zwave-switch/src/test/test_zooz_double_plug.lua index 7826e3d4e4..7725238086 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zooz_double_plug.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zooz_double_plug.lua @@ -73,7 +73,7 @@ test.register_coroutine_test( mock_parent:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -174,7 +174,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -229,7 +229,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -256,7 +256,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -284,7 +284,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -326,7 +326,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -368,7 +368,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -410,7 +410,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -452,7 +452,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -476,7 +476,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -500,7 +500,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -532,7 +532,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -564,7 +564,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zooz_power_strip.lua b/drivers/SmartThings/zwave-switch/src/test/test_zooz_power_strip.lua index 8ffb5f93f6..15ef4f227d 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zooz_power_strip.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zooz_power_strip.lua @@ -85,7 +85,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -124,7 +124,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -202,7 +202,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -241,7 +241,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -280,7 +280,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -319,7 +319,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -359,7 +359,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -399,7 +399,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -439,7 +439,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -483,7 +483,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -526,7 +526,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -569,7 +569,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -612,7 +612,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -655,7 +655,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -698,7 +698,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -741,7 +741,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -784,7 +784,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -827,7 +827,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -870,7 +870,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -932,7 +932,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switch.switch.off())) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1011,7 +1011,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1090,7 +1090,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1126,7 +1126,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1162,7 +1162,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1198,7 +1198,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1234,7 +1234,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1270,7 +1270,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1306,7 +1306,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1342,7 +1342,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1378,7 +1378,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1414,7 +1414,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -1450,7 +1450,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay.lua b/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay.lua index b42d511166..40105722ac 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay.lua @@ -94,7 +94,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -178,7 +178,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -218,7 +218,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -263,7 +263,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -312,7 +312,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -348,7 +348,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -380,7 +380,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -412,7 +412,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -443,7 +443,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -474,7 +474,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -505,7 +505,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -536,7 +536,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -572,7 +572,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -621,7 +621,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -648,7 +648,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -675,7 +675,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -702,7 +702,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -729,7 +729,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -756,7 +756,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -783,7 +783,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -810,7 +810,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -837,7 +837,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -864,7 +864,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -891,7 +891,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -918,7 +918,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -945,7 +945,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -972,7 +972,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -999,7 +999,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1026,7 +1026,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1053,7 +1053,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1080,7 +1080,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1107,7 +1107,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1129,7 +1129,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1157,7 +1157,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1185,7 +1185,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1224,7 +1224,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1272,7 +1272,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -1291,7 +1291,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay_preferences.lua b/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay_preferences.lua index b021a25aa4..ab3923bb2a 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay_preferences.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zooz_zen_30_dimmer_relay_preferences.lua @@ -79,7 +79,7 @@ do test.wait_for_events() end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dimmer_power_energy.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dimmer_power_energy.lua index cd47374f82..efb6bd1ee2 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dimmer_power_energy.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dimmer_power_energy.lua @@ -72,7 +72,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -157,7 +157,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -214,7 +214,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -246,7 +246,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -270,7 +270,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -339,7 +339,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -408,7 +408,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -441,7 +441,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch.lua index 16b15348ce..989bc0f054 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch.lua @@ -82,7 +82,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -150,7 +150,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -184,7 +184,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -218,7 +218,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -252,7 +252,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -286,7 +286,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -320,7 +320,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -360,7 +360,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -400,7 +400,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -440,7 +440,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -480,7 +480,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -520,7 +520,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -560,7 +560,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -600,7 +600,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -640,7 +640,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -664,7 +664,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -688,7 +688,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch_migration.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch_migration.lua index d11dd78b8c..ef2e4997b2 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch_migration.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_dual_switch_migration.lua @@ -74,7 +74,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -101,7 +101,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua index 15fbe8b065..7c8da1f4ef 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch.lua @@ -78,7 +78,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -105,7 +105,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -129,7 +129,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -178,7 +178,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -204,7 +204,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -238,7 +238,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -272,7 +272,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -306,7 +306,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -340,7 +340,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_battery.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_battery.lua index 608d92a052..ad20f7a864 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_battery.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_battery.lua @@ -45,7 +45,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -76,7 +76,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -110,7 +110,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_electric_meter.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_electric_meter.lua index 5ec6b0a340..e008f82e94 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_electric_meter.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_electric_meter.lua @@ -57,7 +57,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -79,7 +79,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -118,7 +118,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -160,7 +160,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_energy_meter.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_energy_meter.lua index f1b382ee67..ad3597766c 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_energy_meter.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_energy_meter.lua @@ -63,7 +63,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -85,7 +85,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -150,7 +150,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -162,7 +162,7 @@ test.register_coroutine_test( test.socket.zwave:__expect_send(zw_test_utils.zwave_test_build_send_command( mock_switch, Meter:Get({scale = Meter.scale.electric_meter.KILOWATT_HOURS}))) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_level.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_level.lua index 1841b7c97e..53b09297a0 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_level.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_level.lua @@ -46,7 +46,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -72,7 +72,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_power_meter.lua b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_power_meter.lua index 7ea67ed56d..fef5989b44 100644 --- a/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_power_meter.lua +++ b/drivers/SmartThings/zwave-switch/src/test/test_zwave_switch_power_meter.lua @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -85,7 +85,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -116,7 +116,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -150,7 +150,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua index 341926c301..9bb950ffca 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_aeotec_radiator_thermostat.lua @@ -95,7 +95,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -114,7 +114,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -133,7 +133,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -164,7 +164,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -184,7 +184,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -207,7 +207,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -235,7 +235,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -268,7 +268,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_ct100_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_ct100_thermostat.lua index 29645b580d..dce072706c 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_ct100_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_ct100_thermostat.lua @@ -137,7 +137,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -165,7 +165,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -221,7 +221,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -280,7 +280,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -339,7 +339,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -404,7 +404,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -473,7 +473,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -499,7 +499,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -524,7 +524,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_fibaro_heat_controller.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_fibaro_heat_controller.lua index eee99d0445..8f5f9011e1 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_fibaro_heat_controller.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_fibaro_heat_controller.lua @@ -116,7 +116,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -174,7 +174,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -216,7 +216,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -235,7 +235,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -266,7 +266,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -286,7 +286,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -309,7 +309,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -355,7 +355,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -383,7 +383,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -416,7 +416,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua index 76b6dcc423..2c74e4c5be 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_popp_radiator_thermostat.lua @@ -62,7 +62,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -81,7 +81,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -135,7 +135,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -159,7 +159,7 @@ test.register_coroutine_test( test.mock_time.advance_time(200) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -203,7 +203,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_qubino_flush_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_qubino_flush_thermostat.lua index d2cc7cd938..a1842ae574 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_qubino_flush_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_qubino_flush_thermostat.lua @@ -123,7 +123,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -172,7 +172,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -193,7 +193,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -213,7 +213,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -240,7 +240,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -267,7 +267,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -324,7 +324,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -376,7 +376,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -404,7 +404,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -437,7 +437,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -465,7 +465,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -487,7 +487,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -517,7 +517,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -570,7 +570,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_stelpro_ki_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_stelpro_ki_thermostat.lua index 2af3f3e24c..bb4043f969 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_stelpro_ki_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_stelpro_ki_thermostat.lua @@ -68,7 +68,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -107,7 +107,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -146,7 +146,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -190,7 +190,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -234,7 +234,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -273,7 +273,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -312,7 +312,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -330,7 +330,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -351,7 +351,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -377,7 +377,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_thermostat_heating_battery.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_thermostat_heating_battery.lua index bf6fc0e42e..70ec5c90d2 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_thermostat_heating_battery.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_thermostat_heating_battery.lua @@ -104,7 +104,7 @@ test.register_coroutine_test( do_initial_setup() end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -123,7 +123,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -155,7 +155,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -217,7 +217,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -239,7 +239,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -271,7 +271,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -303,7 +303,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -332,7 +332,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -425,7 +425,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -539,7 +539,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -642,7 +642,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -728,7 +728,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -770,7 +770,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -810,7 +810,7 @@ test.register_coroutine_test( end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua b/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua index 900331c254..f4d0a6c811 100644 --- a/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua +++ b/drivers/SmartThings/zwave-thermostat/src/test/test_zwave_thermostat.lua @@ -144,7 +144,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -163,7 +163,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -182,7 +182,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -201,7 +201,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -226,7 +226,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -250,7 +250,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -281,7 +281,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 15 } ) @@ -304,7 +304,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -324,7 +324,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -344,7 +344,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -367,7 +367,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -390,7 +390,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -418,7 +418,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -446,7 +446,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -474,7 +474,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -502,7 +502,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -530,7 +530,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -558,7 +558,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -591,7 +591,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -645,7 +645,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -698,7 +698,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -753,7 +753,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua b/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua index 801a1608eb..7e53d6e943 100644 --- a/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua +++ b/drivers/SmartThings/zwave-valve/src/test/test_inverse.valve.lua @@ -49,7 +49,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -70,7 +70,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -91,7 +91,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -112,7 +112,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -145,7 +145,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -178,7 +178,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua b/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua index 3743f62fd5..1b689e6903 100644 --- a/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua +++ b/drivers/SmartThings/zwave-valve/src/test/test_zwave_valve.lua @@ -71,7 +71,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -90,7 +90,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -121,7 +121,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -152,7 +152,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -185,7 +185,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -218,7 +218,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -251,7 +251,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -284,7 +284,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-virtual-momentary-switch/src/test/test_zwave_virtual_momentary_switch.lua b/drivers/SmartThings/zwave-virtual-momentary-switch/src/test/test_zwave_virtual_momentary_switch.lua index e496b5a629..0971b8653e 100644 --- a/drivers/SmartThings/zwave-virtual-momentary-switch/src/test/test_zwave_virtual_momentary_switch.lua +++ b/drivers/SmartThings/zwave-virtual-momentary-switch/src/test/test_zwave_virtual_momentary_switch.lua @@ -67,7 +67,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -86,7 +86,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -113,7 +113,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -136,7 +136,7 @@ test.register_message_test( }, { inner_block_ordering = "relaxed", - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -236,7 +236,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -266,7 +266,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_fibaro_roller_shutter.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_fibaro_roller_shutter.lua index ffeb175b43..85567ad47d 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_fibaro_roller_shutter.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_fibaro_roller_shutter.lua @@ -68,7 +68,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -94,7 +94,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -120,7 +120,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -153,7 +153,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -186,7 +186,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -219,7 +219,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -253,7 +253,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -287,7 +287,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -309,7 +309,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -343,7 +343,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -377,7 +377,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -405,7 +405,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -431,7 +431,7 @@ test.register_coroutine_test( assert(mock_fibaro_roller_shutter:get_field("calibration") == "done", "Calibration should be done") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -456,7 +456,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -483,7 +483,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -510,7 +510,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -537,7 +537,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -564,7 +564,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -591,7 +591,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -618,7 +618,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -645,7 +645,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -672,7 +672,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -699,7 +699,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -726,7 +726,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -753,7 +753,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -780,7 +780,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -807,7 +807,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -834,7 +834,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -851,7 +851,7 @@ test.register_coroutine_test( mock_fibaro_roller_shutter_venetian:expect_metadata_update({ profile = "fibaro-roller-shutter" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -867,7 +867,7 @@ test.register_coroutine_test( mock_fibaro_roller_shutter_venetian:expect_metadata_update({ profile = "fibaro-roller-shutter-venetian" }) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_qubino_flush_shutter.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_qubino_flush_shutter.lua index 4127e8f7c4..9208b2d198 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_qubino_flush_shutter.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_qubino_flush_shutter.lua @@ -76,7 +76,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -109,7 +109,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -142,7 +142,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -175,7 +175,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -208,7 +208,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -230,7 +230,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -264,7 +264,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -297,7 +297,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -325,7 +325,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -358,7 +358,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -383,7 +383,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -410,7 +410,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -437,7 +437,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -464,7 +464,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -491,7 +491,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -518,7 +518,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -563,7 +563,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -582,7 +582,7 @@ test.register_coroutine_test( mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter-venetian" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -601,7 +601,7 @@ test.register_coroutine_test( mock_qubino_flush_shutter:expect_metadata_update({ profile = "qubino-flush-shutter" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -623,7 +623,7 @@ test.register_coroutine_test( assert(targetValue == mock_qubino_flush_shutter.transient_store.shade_target, "driver should chache correct level value") end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -653,7 +653,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -686,7 +686,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -713,7 +713,7 @@ test.register_message_test( } }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -739,7 +739,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 15 } ) @@ -772,7 +772,7 @@ do ) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_aeotec_nano_shutter.lua b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_aeotec_nano_shutter.lua index 8f6442b0ee..b2b968750c 100644 --- a/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_aeotec_nano_shutter.lua +++ b/drivers/SmartThings/zwave-window-treatment/src/test/test_zwave_aeotec_nano_shutter.lua @@ -48,7 +48,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -69,7 +69,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -102,7 +102,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -141,7 +141,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -170,7 +170,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -199,7 +199,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -240,7 +240,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -287,7 +287,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -301,7 +301,7 @@ test.register_coroutine_test( mock_window_button:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -334,7 +334,7 @@ test.register_message_test( }, }, { - min_api_version = 17 + min_api_version = 14 } ) @@ -349,7 +349,7 @@ test.register_coroutine_test( ) end, { - min_api_version = 17 + min_api_version = 14 } ) @@ -369,7 +369,7 @@ test.register_coroutine_test( )) end, { - min_api_version = 17 + min_api_version = 14 } ) diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua index 1cf3335d9e..23f96507ca 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_curtain.lua @@ -59,7 +59,10 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ mock_simple_device.id, zigbee_test_utils.build_bind_request(mock_simple_device, zigbee_test_utils.mock_hub_eui, Basic.ID) }) test.socket.zigbee:__expect_send({ mock_simple_device.id, Basic.attributes.ApplicationVersion:configure_reporting(mock_simple_device, 30, 300, 1) }) mock_simple_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( @@ -89,7 +92,10 @@ test.register_coroutine_test( tuya_utils.build_send_tuya_command(mock_simple_device, '\x05', tuya_utils.DP_TYPE_ENUM, '\x00', 1) } ) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( @@ -131,7 +137,10 @@ test.register_coroutine_test( test.socket.capability:__expect_send( mock_simple_device:generate_test_message("main", capabilities.windowShadePreset.supportedCommands({"presetPosition", "setPresetPosition"}, {visibility = {displayed=false}})) ) - end + end, + { + min_api_version = 17 + } ) test.register_message_test( @@ -152,7 +161,10 @@ test.register_message_test( direction = "send", message = mock_simple_device:generate_test_message("main", capabilities.windowShade.windowShade.opening()) } - } + }, + { + min_api_version = 17 + } ) test.register_message_test( @@ -173,7 +185,10 @@ test.register_message_test( direction = "send", message = mock_simple_device:generate_test_message("main", capabilities.windowShade.windowShade.closing()) } - } + }, + { + min_api_version = 17 + } ) test.register_message_test( @@ -189,7 +204,10 @@ test.register_message_test( direction = "send", message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x01', tuya_utils.DP_TYPE_ENUM, '\x01', 0) } } - } + }, + { + min_api_version = 17 + } ) test.register_message_test( @@ -205,7 +223,10 @@ test.register_message_test( direction = "send", message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x3c', 0) } } - } + }, + { + min_api_version = 17 + } ) test.register_message_test( @@ -221,7 +242,10 @@ test.register_message_test( direction = "send", message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x32', 0) } } - } + }, + { + min_api_version = 17 + } ) test.register_message_test( @@ -242,7 +266,10 @@ test.register_message_test( direction = "send", message = mock_simple_device:generate_test_message("main", capabilities.windowShade.windowShade("partially open")) } - } + }, + { + min_api_version = 17 + } ) test.register_message_test( @@ -259,7 +286,10 @@ test.register_message_test( -- For _TZE284_nladmfvf, level is inverted: 100 - 10 = 90 (0x5A) message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x5a', 0) } } - } + }, + { + min_api_version = 17 + } ) test.register_message_test( @@ -276,7 +306,10 @@ test.register_message_test( -- For _TZE284_nladmfvf, level is inverted: 100 - 0 = 100 (0x64), but clamped to 0 so 100 - 0 = 100 message = { mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x64', 0) } } - } + }, + { + min_api_version = 17 + } ) test.register_coroutine_test( @@ -305,7 +338,10 @@ test.register_coroutine_test( mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x28', 0) }) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( @@ -334,7 +370,10 @@ test.register_coroutine_test( mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x00', 0) }) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( @@ -363,7 +402,10 @@ test.register_coroutine_test( mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x64', 0) }) - end + end, + { + min_api_version = 17 + } ) test.register_coroutine_test( @@ -405,7 +447,10 @@ test.register_coroutine_test( mock_simple_device.id, tuya_utils.build_send_tuya_command(mock_simple_device, '\x02', tuya_utils.DP_TYPE_VALUE, '\x00\x00\x00\x28', 1) }) - end + end, + { + min_api_version = 17 + } ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua index 97537c36ad..da6c87843a 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_switch.lua @@ -254,6 +254,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } + }, + { + min_api_version = 17 } ) @@ -270,6 +273,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } + }, + { + min_api_version = 17 } ) @@ -286,6 +292,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } + }, + { + min_api_version = 17 } ) @@ -302,6 +311,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } + }, + { + min_api_version = 17 } ) @@ -318,6 +330,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x01', 0) } } + }, + { + min_api_version = 17 } ) @@ -334,6 +349,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x01', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } + }, + { + min_api_version = 17 } ) @@ -350,6 +368,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x02', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } + }, + { + min_api_version = 17 } ) @@ -366,6 +387,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x03', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } + }, + { + min_api_version = 17 } ) @@ -382,6 +406,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x04', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } + }, + { + min_api_version = 17 } ) @@ -398,6 +425,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x05', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } + }, + { + min_api_version = 17 } ) @@ -414,6 +444,9 @@ test.register_message_test( direction = "send", message = { mock_parent_device.id, tuya_utils.build_send_tuya_command(mock_parent_device, '\x06', tuya_utils.DP_TYPE_BOOL, '\x00', 0) } } + }, + { + min_api_version = 17 } ) @@ -460,4 +493,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() \ No newline at end of file +test.run_registered_tests() diff --git a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_thermostat.lua b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_thermostat.lua index 4df634bf18..6198f22f94 100644 --- a/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_thermostat.lua +++ b/drivers/Unofficial/tuya-zigbee/src/test/test_tuya_thermostat.lua @@ -166,7 +166,9 @@ test.register_message_test( } } }, - {} + { + min_api_version = 17 + } ) test.register_message_test( @@ -200,7 +202,9 @@ test.register_message_test( } } }, - {} + { + min_api_version = 17 + } ) test.register_message_test( @@ -234,7 +238,9 @@ test.register_message_test( } } }, - {} + { + min_api_version = 17 + } ) test.register_message_test( @@ -268,7 +274,9 @@ test.register_message_test( } } }, - {} + { + min_api_version = 17 + } ) test.register_message_test( @@ -302,7 +310,9 @@ test.register_message_test( } } }, - {} + { + min_api_version = 17 + } ) test.register_message_test( @@ -336,7 +346,9 @@ test.register_message_test( } } }, - {} + { + min_api_version = 17 + } ) test.register_message_test( From d934797966cc312862659900343147e8b200bd01 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Tue, 18 Aug 2026 11:46:03 -0500 Subject: [PATCH 090/102] WWSTCERT-12230 IP65 Outdoor Zwave Smart Plug with 2 Individual Outlets (#3113) --- drivers/SmartThings/zwave-switch/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/zwave-switch/fingerprints.yml b/drivers/SmartThings/zwave-switch/fingerprints.yml index 2ca0fa3c73..c9e58b38f1 100644 --- a/drivers/SmartThings/zwave-switch/fingerprints.yml +++ b/drivers/SmartThings/zwave-switch/fingerprints.yml @@ -879,6 +879,12 @@ zwaveManufacturer: productType: 0xFF00 productId: 0xFF0E deviceProfileName: metering-switch + - id: "0312/FF01/FF97" + deviceLabel: IP65 Outdoor Zwave Smart Plug with 2 Individual Outlets + manufacturerId: 0x0312 + productId: 0xFF97 + productType: 0xFF01 + deviceProfileName: switch-binary - id: Evolve/RelaySwitch deviceLabel: Evolve Switch manufacturerId: 0x0113 From b92335eff9d6e783e48f0a08dd80d8da3156aa30 Mon Sep 17 00:00:00 2001 From: seojune79 <119141897+seojune79@users.noreply.github.com> Date: Wed, 19 Aug 2026 03:29:48 +0900 Subject: [PATCH 091/102] [Aqara] Aqara Dual Relay Module T2 (#3146) * add Aqara Dual Relay Module T2 * restore previous naming convention * improve test cases * correct incorrect model information * remove whitespace * Changed the single-element table to a boolean flag for coercing the preference value. * Add comment explaining omission of unsupported mode --- .../zigbee-switch/fingerprints.yml | 5 + .../aqara-dual-relay-module-unified.yml | 40 +++ .../zigbee-switch/src/aqara/can_handle.lua | 2 + .../zigbee-switch/src/aqara/fingerprints.lua | 37 +-- .../zigbee-switch/src/aqara/init.lua | 135 ++++++++- .../src/aqara/multi-switch/can_handle.lua | 2 + .../src/aqara/multi-switch/fingerprints.lua | 19 +- .../src/aqara/multi-switch/init.lua | 43 ++- .../src/test/test_aqara_dual_relay_module.lua | 268 ++++++++++++++++++ .../src/test/test_aqara_switch_no_power.lua | 89 +++--- .../src/test/test_aqara_switch_power.lua | 59 +++- 11 files changed, 606 insertions(+), 93 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/profiles/aqara-dual-relay-module-unified.yml create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_aqara_dual_relay_module.lua diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index 08eb875591..bebe5295a2 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -111,6 +111,11 @@ zigbeeManufacturer: manufacturer: LUMI model: lumi.switch.n0acn2 deviceProfileName: aqara-switch-module + - id: "Aqara/lumi.switch.acn047" + deviceLabel: Aqara Dual Relay Module T2 (With Neutral) 1 + manufacturer: Aqara + model: lumi.switch.acn047 + deviceProfileName: aqara-dual-relay-module-unified - id: "LUMI/lumi.light.acn004" deviceLabel: Aqara Smart Dimmer Controller T1 Pro manufacturer: LUMI diff --git a/drivers/SmartThings/zigbee-switch/profiles/aqara-dual-relay-module-unified.yml b/drivers/SmartThings/zigbee-switch/profiles/aqara-dual-relay-module-unified.yml new file mode 100644 index 0000000000..b77d7eac0f --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/aqara-dual-relay-module-unified.yml @@ -0,0 +1,40 @@ +name: aqara-dual-relay-module-unified +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: button + version: 1 + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + categories: + - name: Switch + - id: interlock + capabilities: + - id: mode + version: 1 + categories: + - name: Switch + - id: devicemode + capabilities: + - id: mode + version: 1 + categories: + - name: Switch +preferences: + - preferenceId: stse.powerOffMemory + explicit: true + - preferenceId: stse.pulseIntervalSetting + explicit: true + - preferenceId: stse.switchType + explicit: true + - preferenceId: stse.changeToWirelessSwitch + explicit: true +metadata: + mnmn: SolutionsEngineering + vid: SmartThings-smartthings-Aqara_Dual_Relay_Module_T2 diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua index e183f78266..c0eb27343e 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/can_handle.lua @@ -1,6 +1,8 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +-- Matches any device whose manufacturer/model is listed in aqara/fingerprints.lua, routing it to +-- the aqara sub-driver (and, in turn, its version / multi-switch sub-drivers). return function(opts, driver, device) local FINGERPRINTS = require("aqara.fingerprints") for _, fingerprint in ipairs(FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua index 63f94e2be3..3fc2fbc3e3 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/fingerprints.lua @@ -2,21 +2,22 @@ -- Licensed under the Apache License, Version 2.0 return { - { mfr = "LUMI", model = "lumi.plug.maeu01" }, - { mfr = "LUMI", model = "lumi.plug.macn01" }, - { mfr = "LUMI", model = "lumi.switch.n0agl1" }, - { mfr = "LUMI", model = "lumi.switch.l0agl1" }, - { mfr = "LUMI", model = "lumi.switch.n0acn2" }, - { mfr = "LUMI", model = "lumi.switch.n1acn1" }, - { mfr = "LUMI", model = "lumi.switch.n2acn1" }, - { mfr = "LUMI", model = "lumi.switch.n3acn1" }, - { mfr = "LUMI", model = "lumi.switch.b1laus01" }, - { mfr = "LUMI", model = "lumi.switch.b2laus01" }, - { mfr = "LUMI", model = "lumi.switch.n1aeu1" }, - { mfr = "LUMI", model = "lumi.switch.n2aeu1" }, - { mfr = "LUMI", model = "lumi.switch.l1aeu1" }, - { mfr = "LUMI", model = "lumi.switch.l2aeu1" }, - { mfr = "LUMI", model = "lumi.switch.b1nacn01" }, - { mfr = "LUMI", model = "lumi.switch.b2nacn01" }, - { mfr = "LUMI", model = "lumi.switch.b3n01" } - } + { mfr = "LUMI", model = "lumi.plug.maeu01" }, + { mfr = "LUMI", model = "lumi.plug.macn01" }, + { mfr = "LUMI", model = "lumi.switch.n0agl1" }, + { mfr = "LUMI", model = "lumi.switch.l0agl1" }, + { mfr = "LUMI", model = "lumi.switch.n0acn2" }, + { mfr = "LUMI", model = "lumi.switch.n1acn1" }, + { mfr = "LUMI", model = "lumi.switch.n2acn1" }, + { mfr = "LUMI", model = "lumi.switch.n3acn1" }, + { mfr = "LUMI", model = "lumi.switch.b1laus01" }, + { mfr = "LUMI", model = "lumi.switch.b2laus01" }, + { mfr = "LUMI", model = "lumi.switch.n1aeu1" }, + { mfr = "LUMI", model = "lumi.switch.n2aeu1" }, + { mfr = "LUMI", model = "lumi.switch.l1aeu1" }, + { mfr = "LUMI", model = "lumi.switch.l2aeu1" }, + { mfr = "LUMI", model = "lumi.switch.b1nacn01" }, + { mfr = "LUMI", model = "lumi.switch.b2nacn01" }, + { mfr = "LUMI", model = "lumi.switch.b3n01" }, + { mfr = "Aqara", model = "lumi.switch.acn047" } +} diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua index 0c64b594fe..d3666a3dcb 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/init.lua @@ -23,9 +23,31 @@ local CHANGE_TO_WIRELESS_SWITCH_ATTRIBUTE_ID = 0x0200 local RESTORE_TURN_OFF_INDICATOR_LIGHT_ATTRIBUTE_ID = 0x0203 local MAX_POWER_ATTRIBUTE_ID = 0x020B local ELECTRIC_SWITCH_TYPE_ATTRIBUTE_ID = 0x000A - +-- Aqara private cluster (0xFCC0) attributes used by the Dual Relay Module T2 (lumi.switch.acn047) +local DEVICE_MODE_ATTRIBUTE_ID = 0x0289 -- relay working mode (wet/dry contact, pulse), Uint8 0..3 +local INTERLOCK_ATTRIBUTE_ID = 0x02D0 -- interlock between the two relays, Boolean +local POWER_OFF_MEMORY_ATTRIBUTE_ID = 0x0517 -- power-off memory behavior, Uint8 (see powerOffMemory value_map) +local PULSE_INTERVAL_ATTRIBUTE_ID = 0x00EB -- pulse width in ms when running in pulse mode, Uint16 local LAST_REPORT_TIME = "LAST_REPORT_TIME" local PRIVATE_MODE = "PRIVATE_MODE" +-- "interlock" / "devicemode" are extra profile components on aqara-dual-relay-module-unified.yml. +-- The order of the SUPPORTED_* lists matches the raw device values (0-based), see the handlers below. +local COMPONENT_INTERLOCK = "interlock" +local SUPPORTED_INTERLOCK = { "normal", "interlock" } +local COMPONENT_DEVICE_MODE = "devicemode" +-- Relay working modes mapped to their raw device values. dry_contact_open_pulse_mode (raw 2) is +-- intentionally not exposed; the remaining modes keep their original device values (on_off stays 3). +-- This mode is not supported by the Aqara app and cannot be entered by the device, so it is omitted. +local DEVICE_MODE_TO_VALUE = { + wet_contact_mode = 0, + dry_contact_closed_pulse_mode = 1, + dry_contact_on_off_mode = 3, +} +local DEVICE_MODE_FROM_VALUE = { + [0] = "wet_contact_mode", + [1] = "dry_contact_closed_pulse_mode", + [3] = "dry_contact_on_off_mode", +} local preference_map = { ["stse.restorePowerState"] = { @@ -39,7 +61,7 @@ local preference_map = { attribute_id = CHANGE_TO_WIRELESS_SWITCH_ATTRIBUTE_ID, mfg_code = MFG_CODE, data_type = data_types.Uint8, - value_map = { [true] = 0x00,[false] = 0x01 }, + value_map = { [true] = 0x00, [false] = 0x01 }, }, ["stse.maxPower"] = { cluster_id = PRIVATE_CLUSTER_ID, @@ -112,26 +134,71 @@ local preference_map = { data_type = data_types.Uint8, value_map = { rocker = 0x01, rebound = 0x02 }, }, + -- External switch wiring type (same attribute as stse.electricSwitchType, with an extra "nc" + -- option): rocker = maintained, rebound/button = momentary, nc = external switch ignored (normally closed). + ["stse.switchType"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = ELECTRIC_SWITCH_TYPE_ATTRIBUTE_ID, + mfg_code = MFG_CODE, + data_type = data_types.Uint8, + value_map = { rocker = 0x01, rebound = 0x02, nc = 0x00 }, + }, ["stse.turnOffIndicatorLight"] = { cluster_id = PRIVATE_CLUSTER_ID, attribute_id = RESTORE_TURN_OFF_INDICATOR_LIGHT_ATTRIBUTE_ID, mfg_code = MFG_CODE, data_type = data_types.Boolean, }, + ["stse.powerOffMemory"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = POWER_OFF_MEMORY_ATTRIBUTE_ID, + mfg_code = MFG_CODE, + data_type = data_types.Uint8, + value_map = { restore = 0x01, on = 0x00, off = 0x02, reverse = 0x03 }, + }, + ["stse.pulseIntervalSetting"] = { + cluster_id = PRIVATE_CLUSTER_ID, + attribute_id = PULSE_INTERVAL_ATTRIBUTE_ID, + mfg_code = MFG_CODE, + data_type = data_types.Uint16, + coerce_number = true, + } } - +-- Handles reports of the private-mode attribute. Caches the current private-mode state and, when the +-- device is not yet in private mode, forces it into private mode and configures energy reporting. +-- acn047 is excluded from being forced into private mode (it stays on standard clusters). local function private_mode_handler(driver, device, value, zb_rx) device:set_field(PRIVATE_MODE, value.value, { persist = true }) if value.value ~= 1 then - device:send(cluster_base.write_manufacturer_specific_attribute(device, - PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01)) -- private + if device:get_model() ~= "lumi.switch.acn047" then + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01)) -- private + end device:send(SimpleMetering.attributes.CurrentSummationDelivered:configure_reporting(device, 900, 3600, 1)) -- minimal interval : 15min device:set_field(constants.ELECTRICAL_MEASUREMENT_DIVISOR_KEY, 10, { persist = true }) device:set_field(constants.SIMPLE_METERING_DIVISOR_KEY, 1000, { persist = true }) end end +-- Reflect the device's interlock state onto the "interlock" component. +-- value.value is a Boolean; +1 converts the 0/1 state into a Lua (1-based) list index. +local function interlock_switch_handler(driver, device, value, zb_rx) + local component = device.profile.components[COMPONENT_INTERLOCK] + if component == nil then return end + local cur_state = 0 + if value.value then cur_state = 1 end + device:emit_component_event(component, capabilities.mode.mode(SUPPORTED_INTERLOCK[cur_state + 1])) +end +-- Reflect the relay working mode onto the "devicemode" component. +-- value.value is the raw device value; only emit for modes we expose (open_pulse is ignored). +local function device_mode_handler(driver, device, value, zb_rx) + local component = device.profile.components[COMPONENT_DEVICE_MODE] + if component == nil then return end + local mode = DEVICE_MODE_FROM_VALUE[value.value] + if mode ~= nil then + device:emit_component_event(component, capabilities.mode.mode(mode)) + end +end local function wireless_switch_handler(driver, device, value, zb_rx) if value.value == 1 then @@ -151,7 +218,7 @@ local function energy_meter_power_consumption_report(driver, device, value, zb_r if raw_value < offset then --- somehow our value has gone below the offset, so we'll reset the offset, since the device seems to have offset = 0 - device:set_field(constants.ENERGY_METER_OFFSET, offset, {persist = true}) + device:set_field(constants.ENERGY_METER_OFFSET, offset, { persist = true }) end device:emit_event(capabilities.energyMeter.energy({ value = raw_value - offset, unit = "Wh" })) @@ -166,8 +233,7 @@ local function energy_meter_power_consumption_report(driver, device, value, zb_r -- power consumption report local delta_energy = 0.0 - local current_power_consumption = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, - capabilities.powerConsumptionReport.powerConsumption.NAME) + local current_power_consumption = device:get_latest_state("main", capabilities.powerConsumptionReport.ID, capabilities.powerConsumptionReport.powerConsumption.NAME) if current_power_consumption ~= nil then delta_energy = math.max(raw_value - current_power_consumption.energy, 0.0) end @@ -184,14 +250,40 @@ local function power_meter_handler(driver, device, value, zb_rx) device:emit_event(capabilities.powerMeter.power({ value = raw_value, unit = "W" })) end +-- setMode command handler shared by the "interlock" and "devicemode" components. +-- The target component is used to decide which private-cluster attribute to write. +local function mode_handler(driver, device, command) + if command.component == COMPONENT_INTERLOCK then + -- interlock attribute is a Boolean: true = relays interlocked, false = independent + local interlock_mode = false + if command.args.mode == SUPPORTED_INTERLOCK[2] then interlock_mode = true end + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, INTERLOCK_ATTRIBUTE_ID, MFG_CODE, data_types.Boolean, interlock_mode)) + elseif command.component == COMPONENT_DEVICE_MODE then + -- map the selected mode string to its raw device value + local device_mode = DEVICE_MODE_TO_VALUE[command.args.mode] + if device_mode ~= nil then + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, DEVICE_MODE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, device_mode)) + end + end +end +-- Read back switch state, power/energy (standard clusters) and, when present, the interlock and +-- device-mode private attributes. local function do_refresh(self, device) device:send(OnOff.attributes.OnOff:read(device)) if (device:supports_capability_by_id(capabilities.powerMeter.ID)) then device:send(ElectricalMeasurement.attributes.ActivePower:read(device)) device:send(SimpleMetering.attributes.CurrentSummationDelivered:read(device)) end + if device.profile.components[COMPONENT_INTERLOCK] then + device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, INTERLOCK_ATTRIBUTE_ID, MFG_CODE)) + end + if device.profile.components[COMPONENT_DEVICE_MODE] then + device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, DEVICE_MODE_ATTRIBUTE_ID, MFG_CODE)) + end end +-- On preference change, write any preference whose value changed to its mapped private-cluster +-- attribute (see preference_map). value_map translates enum strings; value_type coerces numbers. local function device_info_changed(driver, device, event, args) local preferences = device.preferences local old_preferences = args.old_st_store.preferences @@ -203,28 +295,36 @@ local function device_info_changed(driver, device, event, args) if attr.value_map ~= nil then value = attr.value_map[value] end - device:send(cluster_base.write_manufacturer_specific_attribute(device, attr.cluster_id, attr.attribute_id, - attr.mfg_code, attr.data_type, value)) + -- numeric preferences (e.g. pulseInterval) are coerced to a Lua number before being written + if attr.coerce_number ~= nil and attr.coerce_number then + value = tonumber(value) + end + device:send(cluster_base.write_manufacturer_specific_attribute(device, attr.cluster_id, attr.attribute_id, attr.mfg_code, attr.data_type, value)) end end end end +-- Standard configuration: bind/report standard clusters, read the private-mode attribute, clear any +-- groups (required by these devices) and refresh current state. local function do_configure(self, device) device:configure() - device:send(cluster_base.read_manufacturer_specific_attribute(device, - PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE)) device:send(Groups.server.commands.RemoveAllGroups(device)) -- required do_refresh(self, device) end +-- On add, advertise supported button values and restore the last known power/energy (so the values +-- are not blanked to 0 on re-add). local function device_added(driver, device) if (device:supports_capability_by_id(capabilities.button.ID)) then device:emit_event(capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } })) end if (device:supports_capability_by_id(capabilities.powerMeter.ID)) then - device:emit_event(capabilities.powerMeter.power({ value = 0.0, unit = "W" })) - device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + local lastPower = device:get_latest_state("main", capabilities.powerMeter.ID, capabilities.powerMeter.power.NAME) or 0.0 + local lastEnergy = device:get_latest_state("main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) or 0.0 + device:emit_event(capabilities.powerMeter.power({ value = lastPower, unit = "W" })) + device:emit_event(capabilities.energyMeter.energy({ value = lastEnergy, unit = "Wh" })) end end @@ -236,6 +336,9 @@ local aqara_switch_handler = { infoChanged = device_info_changed }, capability_handlers = { + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = mode_handler + }, [capabilities.refresh.ID] = { [capabilities.refresh.commands.refresh.NAME] = do_refresh } @@ -252,7 +355,9 @@ local aqara_switch_handler = { [WIRELESS_SWITCH_ATTRIBUTE_ID] = wireless_switch_handler }, [PRIVATE_CLUSTER_ID] = { - [PRIVATE_ATTRIBUTE_ID] = private_mode_handler + [PRIVATE_ATTRIBUTE_ID] = private_mode_handler, + [INTERLOCK_ATTRIBUTE_ID] = interlock_switch_handler, + [DEVICE_MODE_ATTRIBUTE_ID] = device_mode_handler } } }, diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua index 1524296cff..16d61cfbbf 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/can_handle.lua @@ -1,6 +1,8 @@ -- Copyright 2025 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +-- Matches multi-endpoint Aqara switches/modules listed in aqara/multi-switch/fingerprints.lua +-- (devices that create child devices for their extra switch endpoints). return function(opts, driver, device) local FINGERPRINTS = require("aqara.multi-switch.fingerprints") for _, fingerprint in ipairs(FINGERPRINTS) do diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua index 71ed1b26b3..22887a7e88 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/fingerprints.lua @@ -2,13 +2,14 @@ -- Licensed under the Apache License, Version 2.0 return { - { mfr = "LUMI", model = "lumi.switch.n1acn1", children = 1, child_profile = "" }, - { mfr = "LUMI", model = "lumi.switch.n2acn1", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.n3acn1", children = 3, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.b1laus01", children = 1, child_profile = "" }, - { mfr = "LUMI", model = "lumi.switch.b2laus01", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.l2aeu1", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.n2aeu1", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.b2nacn01", children = 2, child_profile = "aqara-switch-child" }, - { mfr = "LUMI", model = "lumi.switch.b3n01", children = 3, child_profile = "aqara-switch-child" } + { mfr = "LUMI", model = "lumi.switch.n1acn1", children = 1, child_profile = "" }, + { mfr = "LUMI", model = "lumi.switch.n2acn1", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.n3acn1", children = 3, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.b1laus01", children = 1, child_profile = "" }, + { mfr = "LUMI", model = "lumi.switch.b2laus01", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.l2aeu1", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.n2aeu1", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.b2nacn01", children = 2, child_profile = "aqara-switch-child" }, + { mfr = "LUMI", model = "lumi.switch.b3n01", children = 3, child_profile = "aqara-switch-child" }, + { mfr = "Aqara", model = "lumi.switch.acn047", children = 2, child_profile = "aqara-switch-child" } } diff --git a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua index 4b2146f03c..07022c0b67 100644 --- a/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/aqara/multi-switch/init.lua @@ -11,8 +11,13 @@ local switch_utils = require "switch_utils" local PRIVATE_CLUSTER_ID = 0xFCC0 local PRIVATE_ATTRIBUTE_ID = 0x0009 local MFG_CODE = 0x115F +local COMPONENT_INTERLOCK = "interlock" +local SUPPORTED_INTERLOCK = { "normal", "interlock" } +local COMPONENT_DEVICE_MODE = "devicemode" +local SUPPORTED_DEVICE_MODE = { "wet_contact_mode", "dry_contact_closed_pulse_mode", "dry_contact_on_off_mode" } local FINGERPRINTS = require("aqara.multi-switch.fingerprints") +-- Number of switch endpoints (parent + children) for this model, from the fingerprint table. local function get_children_amount(device) for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then @@ -21,6 +26,7 @@ local function get_children_amount(device) end end +-- Profile name to assign to the created child devices for this model. local function get_child_profile_name(device) for _, fingerprint in ipairs(FINGERPRINTS) do if device:get_model() == fingerprint.model then @@ -37,6 +43,7 @@ local function find_child(parent, ep_id) return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) end +-- Create one EDGE_CHILD device per extra switch endpoint and initialize parent-only state. local function device_added(driver, device) -- Only create children for the actual Zigbee device and not the children if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then @@ -60,22 +67,36 @@ local function device_added(driver, device) end -- for wireless button - device:emit_event(capabilities.button.numberOfButtons({ value = children_amount }, - { visibility = { displayed = false } })) - device:emit_event(capabilities.powerMeter.power({ value = 0.0, unit = "W" })) - device:emit_event(capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" })) + device:emit_event(capabilities.button.numberOfButtons({ value = children_amount }, { visibility = { displayed = false } })) - device:send(cluster_base.write_manufacturer_specific_attribute(device, - PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01)) -- private + -- report the static supported modes for the interlock / devicemode components (when present) + if device.profile.components[COMPONENT_INTERLOCK] then + device:emit_component_event(device.profile.components[COMPONENT_INTERLOCK], capabilities.mode.supportedModes(SUPPORTED_INTERLOCK, { visibility = { displayed = false } })) + end + if device.profile.components[COMPONENT_DEVICE_MODE] then + device:emit_component_event(device.profile.components[COMPONENT_DEVICE_MODE], capabilities.mode.supportedModes(SUPPORTED_DEVICE_MODE, { visibility = { displayed = false } })) + end + + -- acn047 (Dual Relay Module T2) must not be forced into Aqara private mode; all other + -- multi-switch models are switched into private mode here. + if device:get_model() ~= "lumi.switch.acn047" then + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01)) -- private + end elseif device.network_type == "DEVICE_EDGE_CHILD" then - device:emit_event(capabilities.button.numberOfButtons({ value = 1 }, - { visibility = { displayed = false } })) + device:emit_event(capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } })) + end + device:emit_event(capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } })) + switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({ state_change = false })) + -- restore the last known power/energy (instead of resetting to 0) so values survive re-adds + if (device:supports_capability_by_id(capabilities.powerMeter.ID)) then + local lastPower = device:get_latest_state("main", capabilities.powerMeter.ID, capabilities.powerMeter.power.NAME) or 0.0 + local lastEnergy = device:get_latest_state("main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) or 0.0 + device:emit_event(capabilities.powerMeter.power({ value = lastPower, unit = "W" })) + device:emit_event(capabilities.energyMeter.energy({ value = lastEnergy, unit = "Wh" })) end - device:emit_event(capabilities.button.supportedButtonValues({ "pushed" }, - { visibility = { displayed = false } })) - switch_utils.emit_event_if_latest_state_missing(device, "main", capabilities.button, capabilities.button.button.NAME, capabilities.button.button.pushed({state_change = false})) end +-- Register the endpoint->child routing function so reports from child endpoints reach the children. local function device_init(self, device) -- for multiple switch if device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_dual_relay_module.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_dual_relay_module.lua new file mode 100644 index 0000000000..3dd95f7bb1 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_dual_relay_module.lua @@ -0,0 +1,268 @@ +-- Copyright 2025 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local test = require "integration_test" +local t_utils = require "integration_test.utils" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" + +local OnOff = clusters.OnOff +local ElectricalMeasurement = clusters.ElectricalMeasurement +local SimpleMetering = clusters.SimpleMetering + +local PRIVATE_CLUSTER_ID = 0xFCC0 +local MFG_CODE = 0x115F +local INTERLOCK_ATTRIBUTE_ID = 0x02D0 +local DEVICE_MODE_ATTRIBUTE_ID = 0x0289 +local POWER_OFF_MEMORY_ATTRIBUTE_ID = 0x0517 +local PULSE_INTERVAL_ATTRIBUTE_ID = 0x00EB +local ELECTRIC_SWITCH_TYPE_ATTRIBUTE_ID = 0x000A +local CHANGE_TO_WIRELESS_SWITCH_ATTRIBUTE_ID = 0x0200 + +local PRIVATE_MODE = "PRIVATE_MODE" + +local SUPPORTED_INTERLOCK = { "normal", "interlock" } +local SUPPORTED_DEVICE_MODE = { "wet_contact_mode", "dry_contact_closed_pulse_mode", "dry_contact_on_off_mode" } + +-- acn047 (Dual Relay Module T2) runs on standard clusters (never Aqara private mode) +local mock_device = test.mock_device.build_test_zigbee_device( + { + profile = t_utils.get_profile_definition("aqara-dual-relay-module-unified.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "Aqara", + model = "lumi.switch.acn047", + server_clusters = { OnOff.ID, ElectricalMeasurement.ID, SimpleMetering.ID } + } + } + } +) + +-- acn047 reports children = 2, so a single child device (relay 2) exists on the second endpoint +local mock_child = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("aqara-switch-child.yml"), + device_network_id = string.format("%04X:%02X", mock_device:get_short_address(), 2), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%02X", 2) +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_child) +end + +test.set_test_init_function(test_init) + +test.register_coroutine_test( + "added: child is reused, button info reported, power/energy restored (0.0 on first add) and no private write", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("interlock", + capabilities.mode.supportedModes(SUPPORTED_INTERLOCK, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("devicemode", + capabilities.mode.supportedModes(SUPPORTED_DEVICE_MODE, { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 0.0, unit = "W" }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" }))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "refresh reads OnOff, power/energy (standard clusters) and the interlock/devicemode attributes", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } }) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, ElectricalMeasurement.attributes.ActivePower:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, SimpleMetering.attributes.CurrentSummationDelivered:read(mock_device) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, INTERLOCK_ATTRIBUTE_ID, MFG_CODE) }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, DEVICE_MODE_ATTRIBUTE_ID, MFG_CODE) }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "switch on command : parent device", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "switch", component = "main", command = "on", args = {} } }) + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.server.commands.On(mock_device) }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "switch off command : child device (endpoint 2)", + function() + test.socket.capability:__queue_receive({ mock_child.id, + { capability = "switch", component = "main", command = "off", args = {} } }) + mock_child:expect_native_cmd_handler_registration("switch", "off") + test.socket.zigbee:__expect_send({ mock_device.id, OnOff.server.commands.Off(mock_device):to_endpoint(0x02) }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "power meter report (standard ElectricalMeasurement cluster)", + function() + mock_device:set_field(PRIVATE_MODE, 0, { persist = true }) + test.socket.zigbee:__queue_receive({ + mock_device.id, + ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_device, 100) + }) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 10.0, unit = "W" })) + ) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "interlock attribute report updates the interlock component", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, { + { INTERLOCK_ATTRIBUTE_ID, data_types.Boolean.ID, true } + }, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("interlock", + capabilities.mode.mode("interlock"))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "device mode attribute report updates the devicemode component", + function() + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, { + { DEVICE_MODE_ATTRIBUTE_ID, data_types.Uint8.ID, 1 } + }, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("devicemode", + capabilities.mode.mode("dry_contact_closed_pulse_mode"))) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "setMode on the interlock component writes the interlock attribute", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "mode", component = "interlock", command = "setMode", args = { "interlock" } } }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, INTERLOCK_ATTRIBUTE_ID, + MFG_CODE, data_types.Boolean, true) }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "setMode on the devicemode component (on/off) writes device value 3", + function() + test.socket.capability:__queue_receive({ mock_device.id, + { capability = "mode", component = "devicemode", command = "setMode", args = { "dry_contact_on_off_mode" } } }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, DEVICE_MODE_ATTRIBUTE_ID, + MFG_CODE, data_types.Uint8, 3) }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "preference powerOffMemory is written on infochanged", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { ["stse.powerOffMemory"] = "on" } + })) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + POWER_OFF_MEMORY_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0) }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "preference pulseInterval is written on infochanged", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { ["stse.pulseIntervalSetting"] = 500 } + })) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + PULSE_INTERVAL_ATTRIBUTE_ID, MFG_CODE, data_types.Uint16, 500) }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "preference switchType (nc) is written on infochanged", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { ["stse.switchType"] = "nc" } + })) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + ELECTRIC_SWITCH_TYPE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0) }) + end, + { + min_api_version = 17 + } +) + +test.register_coroutine_test( + "preference changeToWirelessSwitch is written on infochanged", + function() + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ + preferences = { ["stse.changeToWirelessSwitch"] = true } + })) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + CHANGE_TO_WIRELESS_SWITCH_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0) }) + end, + { + min_api_version = 17 + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua index 4f12a94a55..74e6eaaa6a 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_no_power.lua @@ -75,28 +75,35 @@ end test.set_test_init_function(test_init) test.register_coroutine_test( - "Lifecycle - added test", + "Lifecycle - added test : parent device", function() - -- The initial switch event should be send during the device's first time onboarding + -- The initial switch event should be send during the device's first time onboarding. + -- This profile has no powerMeter/energyMeter, so no meter initialization event may be sent: + -- any extra capability event would fail the strict channel assertions below. test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, - { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 2 }, + { visibility = { displayed = false } }))) test.socket.zigbee:__expect_send({ mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, - { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, - { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 2 }, + { visibility = { displayed = false } }))) test.socket.zigbee:__expect_send({ mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, - { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) end, { min_api_version = 14 @@ -104,23 +111,29 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Lifecycle - added test", + "Lifecycle - added test : child device", function() - -- The initial switch event should be send during the device's first time onboarding + -- The initial switch event should be send during the device's first time onboarding. + -- The child profile has no powerMeter/energyMeter either, so no meter initialization event is sent. test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) - test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }, - { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, - { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) -- Avoid sending the initial switch event after driver switch-over, as the switch-over event itself re-triggers the added lifecycle. test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) - test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.numberOfButtons({ value = 1 }, - { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, - { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 1 }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_child:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) end, { min_api_version = 14 @@ -128,26 +141,30 @@ test.register_coroutine_test( ) test.register_coroutine_test( - "Lifecycle - added test", + "Lifecycle - added test : child device creation", function() -- The initial switch event should be send during the device's first time onboarding test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_base_device.id, "added" }) - mock_base_device:expect_device_create({ - type = "EDGE_CHILD", - label = "Aqara Smart Wall Switch H1 EU (No Neutral, Double Rocker) 2", - profile = "aqara-switch-child", - parent_device_id = mock_base_device.id, - parent_assigned_child_key = "02" - }) - test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 2 }, - { visibility = { displayed = false } }))) + mock_base_device:expect_device_create({ + type = "EDGE_CHILD", + label = "Aqara Smart Wall Switch H1 EU (No Neutral, Double Rocker) 2", + profile = "aqara-switch-child", + parent_device_id = mock_base_device.id, + parent_assigned_child_key = "02" + }) + test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", + capabilities.button.numberOfButtons({ value = 2 }, + { visibility = { displayed = false } }))) test.socket.zigbee:__expect_send({ mock_base_device.id, - cluster_base.write_manufacturer_specific_attribute(mock_base_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, + cluster_base.write_manufacturer_specific_attribute(mock_base_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, + MFG_CODE, data_types.Uint8, 1) }) - test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, - { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) + test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", + capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_base_device:generate_test_message("main", + capabilities.button.button.pushed({ state_change = false }))) end, { min_api_version = 14 diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua index ffe37bb293..9c1f27b33c 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_aqara_switch_power.lua @@ -69,29 +69,79 @@ end test.set_test_init_function(test_init) test.register_coroutine_test( - "Lifecycle - added test", + "Lifecycle - added test : parent device", function() test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 3 }, { visibility = { displayed = false } }))) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, + data_types.Uint8, 1) }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, + { visibility = { displayed = false } }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) + -- On the first onboarding there is no previous power/energy state, so both meters are initialized to 0 test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 0.0, unit = "W" }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 0.0, unit = "Wh" }))) + end, + { + min_api_version = 14 + } +) + +test.register_coroutine_test( + "Lifecycle - added test : power/energy already reported should be restored instead of reset to 0", + function() + mock_device:set_field(PRIVATE_MODE, 1, { persist = true }) + mock_device:set_field(LAST_REPORT_TIME, os.time() - 60 * 20) + test.socket.zigbee:__set_channel_ordering("relaxed") + + -- report power and energy first so the device has a last known meter state + test.socket.zigbee:__queue_receive({ + mock_device.id, + AnalogInput.attributes.PresentValue:build_test_attr_report(mock_device, + SinglePrecisionFloat(0, 9, 0.953125)):from_endpoint(POWER_METER_ENDPOINT) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.powerMeter.power({ value = 1000.0, unit = "W" }))) + test.socket.zigbee:__expect_send({ mock_device.id, + AnalogInput.attributes.PresentValue:read(mock_device):to_endpoint(ENERGY_METER_ENDPOINT) }) + test.wait_for_events() + + test.socket.zigbee:__queue_receive({ + mock_device.id, + AnalogInput.attributes.PresentValue:build_test_attr_report(mock_device, + SinglePrecisionFloat(0, 9, 0.953125)):from_endpoint(ENERGY_METER_ENDPOINT) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.energyMeter.energy({ value = 1000000.0, unit = "Wh" }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.powerConsumptionReport.powerConsumption({ deltaEnergy = 0.0, energy = 1000000.0 }))) + test.wait_for_events() + + -- re-adding the device must restore the reported values rather than blank them out + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.numberOfButtons({ value = 3 }, + { visibility = { displayed = false } }))) test.socket.zigbee:__expect_send({ mock_device.id, cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 1) }) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) - + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.powerMeter.power({ value = 1000.0, unit = "W" }))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.energyMeter.energy({ value = 1000000.0, unit = "Wh" }))) end, { - min_api_version = 14 + min_api_version = 17 } ) test.register_coroutine_test( - "Lifecycle - added test", + "Lifecycle - added test : child device", function() test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.device_lifecycle:__queue_receive({ mock_child.id, "added" }) @@ -100,6 +150,7 @@ test.register_coroutine_test( test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.supportedButtonValues({ "pushed" }, { visibility = { displayed = false } }))) test.socket.capability:__expect_send(mock_child:generate_test_message("main", capabilities.button.button.pushed({ state_change = false }))) + -- the child profile has no powerMeter/energyMeter, so no meter initialization event is sent end, { min_api_version = 14 From 9744a0e5cec63a3377690b5bec753f66381319a4 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Tue, 18 Aug 2026 14:48:16 -0500 Subject: [PATCH 092/102] WWSTCERT-12431 Update smart plug label (#3173) Co-authored-by: Claude Sonnet 5 --- drivers/SmartThings/zwave-switch/fingerprints.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zwave-switch/fingerprints.yml b/drivers/SmartThings/zwave-switch/fingerprints.yml index c9e58b38f1..f606edf119 100644 --- a/drivers/SmartThings/zwave-switch/fingerprints.yml +++ b/drivers/SmartThings/zwave-switch/fingerprints.yml @@ -874,7 +874,7 @@ zwaveManufacturer: productId: 0x1000 deviceProfileName: metering-switch - id: 0312/FF00/FF0E - deviceLabel: Minoston Outlet + deviceLabel: Smart Plug manufacturerId: 0x0312 productType: 0xFF00 productId: 0xFF0E From 92fb3421bcfb70c58cb0224368e1edb75cb8c5e2 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Wed, 19 Aug 2026 12:07:02 -0500 Subject: [PATCH 093/102] WWSTCERT-11699 Z-Wave Scene Control Switch (#2977) * WWSTCERT-11699 Z-Wave Scene Control Switch * Fix newline issue in fingerprints.yml * Update fingerprint ID for Z-Wave Scene Control Switch --- drivers/SmartThings/zwave-button/fingerprints.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zwave-button/fingerprints.yml b/drivers/SmartThings/zwave-button/fingerprints.yml index 6a2995f5cc..df4a621220 100644 --- a/drivers/SmartThings/zwave-button/fingerprints.yml +++ b/drivers/SmartThings/zwave-button/fingerprints.yml @@ -78,4 +78,11 @@ zwaveManufacturer: manufacturerId: 0x0460 productType: 0x0009 productId: 0x0082 - deviceProfileName: multi-button-4-no-battery \ No newline at end of file + deviceProfileName: multi-button-4-no-battery + - id: "0312/0924/D001" + deviceLabel: Z-Wave Scene Control Switch + manufacturerId: 0x0312 + productId: 0xD001 + productType: 0x0924 + deviceProfileName: multi-button-4 + From 53c74eb115b90e5f207c5ee2cd9e35ae4b221b23 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Wed, 19 Aug 2026 12:10:14 -0500 Subject: [PATCH 094/102] WWSTCERT-13083 OSRAM IRIS (#3187) --- drivers/SmartThings/matter-switch/fingerprints.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 6a4321c1a5..29c85f6c79 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -2120,6 +2120,16 @@ matterManufacturer: vendorId: 0x1189 productId: 0x1001 deviceProfileName: light-color-level + - id: "4489/4573" + deviceLabel: OSRAM IRIS + vendorId: 0x1189 + productId: 0x11DD + deviceProfileName: light-color-level + - id: "4489/4572" + deviceLabel: OSRAM GISELA + vendorId: 0x1189 + productId: 0x11DC + deviceProfileName: light-color-level #Shelly - id: "5264/1" deviceLabel: Shelly Plug S MTR Gen3 From 4ea2b77c038312b168b79d4e811d584ed34c8c36 Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Mon, 24 Aug 2026 13:57:05 -0500 Subject: [PATCH 095/102] Fix PTZ bugs with unsupported & unchanged fields (#3192) * Fix PTZ feature naming bug * Fix PTZ optionality for absolute commands as well * Add defensive check on `ib.data.elements` --- .../camera_handlers/attribute_handlers.lua | 13 +- .../camera_handlers/capability_handlers.lua | 30 ++-- .../camera_utils/device_configuration.lua | 8 +- .../src/test/test_matter_camera.lua | 129 +++++++++++++++++- 4 files changed, 150 insertions(+), 30 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua index 53b35712e7..075f5cc487 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/attribute_handlers.lua @@ -292,6 +292,7 @@ function CameraAttributeHandlers.dptz_streams_handler(driver, device, ib, respon end function CameraAttributeHandlers.ptz_position_handler(driver, device, ib, response) + if not ib.data.elements then return end local ptz_map = camera_utils.get_ptz_map(device) local emit_event = function(idx, value) if value ~= ptz_map[idx].current then @@ -300,13 +301,13 @@ function CameraAttributeHandlers.ptz_position_handler(driver, device, ib, respon )) end end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then + if ib.data.elements.pan and camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN) then emit_event(camera_fields.PAN_IDX, ib.data.elements.pan.value) end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then + if ib.data.elements.tilt and camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT) then emit_event(camera_fields.TILT_IDX, ib.data.elements.tilt.value) end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then + if ib.data.elements.zoom and camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then emit_event(camera_fields.ZOOM_IDX, ib.data.elements.zoom.value) end end @@ -317,13 +318,13 @@ function CameraAttributeHandlers.ptz_presets_handler(driver, device, ib, respons for _, v in ipairs(ib.data.elements) do local preset = v.elements local pan, tilt, zoom = 0, 0, 1 - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN) then pan = preset.settings.elements.pan.value end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT) then tilt = preset.settings.elements.tilt.value end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then zoom = preset.settings.elements.zoom.value end table.insert(presets, { id = preset.preset_id.value, label = preset.name.value, pan = pan, tilt = tilt, zoom = zoom }) diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua index a26afd0ca7..738d6d3556 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_handlers/capability_handlers.lua @@ -139,9 +139,11 @@ end CameraCapabilityHandlers.ptz_relative_move_factory = function(index) return function (driver, device, cmd) local endpoint_id = device:component_to_endpoint(cmd.component) - local pan_delta = index == camera_fields.PAN_IDX and cmd.args.delta or 0 - local tilt_delta = index == camera_fields.TILT_IDX and cmd.args.delta or 0 - local zoom_delta = index == camera_fields.ZOOM_IDX and cmd.args.delta or 0 + -- Only the axis being moved should be included; the other two must be omitted (nil) rather + -- than sent as 0, since each delta field's conformance is tied to its own feature bit. + local pan_delta = index == camera_fields.PAN_IDX and cmd.args.delta or nil + local tilt_delta = index == camera_fields.TILT_IDX and cmd.args.delta or nil + local zoom_delta = index == camera_fields.ZOOM_IDX and cmd.args.delta or nil device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove( device, endpoint_id, pan_delta, tilt_delta, zoom_delta )) @@ -151,23 +153,23 @@ end CameraCapabilityHandlers.ptz_set_position_factory = function(command) return function (driver, device, cmd) local ptz_map = camera_utils.get_ptz_map(device) + -- MPTZSetPosition coordinates are optional; + -- a single-axis command should only set (and clamp) the axis it targets. + local pan, tilt, zoom if command == capabilities.mechanicalPanTiltZoom.commands.setPanTiltZoom then - ptz_map[camera_fields.PAN_IDX].current = cmd.args.pan - ptz_map[camera_fields.TILT_IDX].current = cmd.args.tilt - ptz_map[camera_fields.ZOOM_IDX].current = cmd.args.zoom + pan = utils.clamp_value(cmd.args.pan, ptz_map[camera_fields.PAN_IDX].range.minimum, ptz_map[camera_fields.PAN_IDX].range.maximum) + tilt = utils.clamp_value(cmd.args.tilt, ptz_map[camera_fields.TILT_IDX].range.minimum, ptz_map[camera_fields.TILT_IDX].range.maximum) + zoom = utils.clamp_value(cmd.args.zoom, ptz_map[camera_fields.ZOOM_IDX].range.minimum, ptz_map[camera_fields.ZOOM_IDX].range.maximum) elseif command == capabilities.mechanicalPanTiltZoom.commands.setPan then - ptz_map[camera_fields.PAN_IDX].current = cmd.args.pan + pan = utils.clamp_value(cmd.args.pan, ptz_map[camera_fields.PAN_IDX].range.minimum, ptz_map[camera_fields.PAN_IDX].range.maximum) elseif command == capabilities.mechanicalPanTiltZoom.commands.setTilt then - ptz_map[camera_fields.TILT_IDX].current = cmd.args.tilt + tilt = utils.clamp_value(cmd.args.tilt, ptz_map[camera_fields.TILT_IDX].range.minimum, ptz_map[camera_fields.TILT_IDX].range.maximum) else - ptz_map[camera_fields.ZOOM_IDX].current = cmd.args.zoom - end - for _, v in pairs(ptz_map) do - v.current = utils.clamp_value(v.current, v.range.minimum, v.range.maximum) + zoom = utils.clamp_value(cmd.args.zoom, ptz_map[camera_fields.ZOOM_IDX].range.minimum, ptz_map[camera_fields.ZOOM_IDX].range.maximum) end local endpoint_id = device:component_to_endpoint(cmd.component) - device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(device, endpoint_id, - ptz_map[camera_fields.PAN_IDX].current, ptz_map[camera_fields.TILT_IDX].current, ptz_map[camera_fields.ZOOM_IDX].current + device:send(clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition( + device, endpoint_id, pan, tilt, zoom )) end end diff --git a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua index 0619194926..7cb8edd930 100644 --- a/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-switch/src/sub_drivers/camera/camera_utils/device_configuration.lua @@ -46,19 +46,19 @@ end local function build_ptz_supported_attributes(device) local supported_attributes = {} - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPAN) then + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN) then table.insert(supported_attributes, "pan") table.insert(supported_attributes, "panRange") end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MTILT) then + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT) then table.insert(supported_attributes, "tilt") table.insert(supported_attributes, "tiltRange") end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MZOOM) then + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_ZOOM) then table.insert(supported_attributes, "zoom") table.insert(supported_attributes, "zoomRange") end - if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MPRESETS) then + if camera_utils.feature_supported(device, clusters.CameraAvSettingsUserLevelManagement.ID, clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PRESETS) then table.insert(supported_attributes, "presets") table.insert(supported_attributes, "maxPresets") end diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua index 3926b63fe9..020b7bb906 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_camera.lua @@ -167,6 +167,38 @@ local mock_device_no_per_zone_sensitivity = test.mock_device.build_test_matter_d } }) +local mock_device_pan_tilt_only = test.mock_device.build_test_matter_device({ + profile = t_utils.get_profile_definition("camera.yml"), + manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, + matter_version = {hardware = 1, software = 1}, + endpoints = { + { + endpoint_id = 0, + clusters = { + { cluster_id = clusters.Basic.ID, cluster_type = "SERVER" } + }, + device_types = { + { device_type_id = 0x0016, device_type_revision = 1 } -- RootNode + } + }, + { + endpoint_id = CAMERA_EP, + clusters = { + { + cluster_id = clusters.CameraAvSettingsUserLevelManagement.ID, + feature_map = clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PAN | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_TILT | + clusters.CameraAvSettingsUserLevelManagement.types.Feature.MECHANICAL_PRESETS, + cluster_type = "SERVER" + } + }, + device_types = { + {device_type_id = 0x0142, device_type_revision = 1} -- Camera + } + } + } +}) + local mock_device_no_user_defined_zone = test.mock_device.build_test_matter_device({ profile = t_utils.get_profile_definition("camera.yml"), manufacturer_info = {vendor_id = 0x0000, product_id = 0x0000}, @@ -297,6 +329,31 @@ local function test_init_no_user_defined_zone() mock_device_no_user_defined_zone:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end +-- mock_device_pan_tilt_only has no CameraAvStreamManagement cluster at all, so do_configure's +-- `if #device:get_endpoints(clusters.CameraAvStreamManagement.ID) == 0` branch calls match_profile +-- directly and synchronously during doConfigure, rather than waiting on an AttributeList report +-- like the other reduced fixtures above. That means the profile/capability metadata update and the +-- "PROVISIONED" transition both happen back-to-back off of the same doConfigure lifecycle event. +local subscribe_request_pan_tilt_only + +local function test_init_pan_tilt_only() + test.mock_device.add_test_device(mock_device_pan_tilt_only) + test.socket.device_lifecycle:__queue_receive({ mock_device_pan_tilt_only.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_pan_tilt_only.id, "init" }) + subscribe_request_pan_tilt_only = cluster_base.subscribe( + mock_device_pan_tilt_only, nil, camera_fields.CameraAVSULMFeatureMapAttr.cluster, camera_fields.CameraAVSULMFeatureMapAttr.ID + ) + test.socket.matter:__expect_send({mock_device_pan_tilt_only.id, subscribe_request_pan_tilt_only}) + test.socket.device_lifecycle:__queue_receive({ mock_device_pan_tilt_only.id, "doConfigure" }) + mock_device_pan_tilt_only:expect_metadata_update({ + optional_component_capabilities = { + { "main", { "mechanicalPanTiltZoom" } } + }, + profile = "camera" + }) + mock_device_pan_tilt_only:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +end + local additional_subscribed_attributes = { clusters.CameraAvStreamManagement.attributes.HDRModeEnabled, clusters.CameraAvStreamManagement.attributes.ImageRotation, @@ -616,6 +673,40 @@ local function update_device_profile_no_user_defined_zone() test.socket.matter:__expect_send({mock_device_no_user_defined_zone.id, subscribe_request_no_user_defined_zone}) end +local additional_subscribed_attributes_pan_tilt_only = { + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.MaxPresets, + clusters.CameraAvSettingsUserLevelManagement.attributes.ZoomMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.PanMin, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMax, + clusters.CameraAvSettingsUserLevelManagement.attributes.TiltMin, +} + +-- Unlike the other reduced fixtures, mock_device_pan_tilt_only's profile/capability update already +-- happened synchronously during doConfigure (see test_init_pan_tilt_only), so this only needs to +-- simulate the platform pushing the updated profile back down via infoChanged, then confirm the PTZ +-- capability is initialized with pan/tilt/presets only -- no zoom or zoomRange, since this device's +-- CameraAvSettingsUserLevelManagement feature_map never claimed MECHANICAL_ZOOM support. +local function update_device_profile_pan_tilt_only() + local updated_device_profile = t_utils.get_profile_definition( + "camera.yml", {enabled_optional_capabilities = { + { "main", { "mechanicalPanTiltZoom" } } + }} + ) + test.socket.device_lifecycle:__queue_receive(mock_device_pan_tilt_only:generate_info_changed({ profile = updated_device_profile })) + test.socket.capability:__expect_send( + mock_device_pan_tilt_only:generate_test_message("main", capabilities.mechanicalPanTiltZoom.supportedAttributes( + {"pan", "panRange", "tilt", "tiltRange", "presets", "maxPresets"} + )) + ) + for _, attr in ipairs(additional_subscribed_attributes_pan_tilt_only) do + subscribe_request_pan_tilt_only:merge(attr:subscribe(mock_device_pan_tilt_only)) + end + test.socket.matter:__expect_send({mock_device_pan_tilt_only.id, subscribe_request_pan_tilt_only}) +end + -- Matter Handler UTs test.register_coroutine_test( @@ -1358,6 +1449,32 @@ test.register_coroutine_test( } ) +test.register_coroutine_test( + "PTZ Position report with zoom omitted should be handled normally on a device with no zoom feature", + function() + update_device_profile_pan_tilt_only() + test.wait_for_events() + -- This device has no zoom feature, so its MPTZPosition report never has a zoom field; + -- pan/tilt should still be handled normally. + test.socket.matter:__queue_receive({ + mock_device_pan_tilt_only.id, + clusters.CameraAvSettingsUserLevelManagement.attributes.MPTZPosition:build_test_report_data( + mock_device_pan_tilt_only, CAMERA_EP, {pan = 10, tilt = 20}) + }) + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.capability:__expect_send( + mock_device_pan_tilt_only:generate_test_message("main", capabilities.mechanicalPanTiltZoom.pan(10)) + ) + test.socket.capability:__expect_send( + mock_device_pan_tilt_only:generate_test_message("main", capabilities.mechanicalPanTiltZoom.tilt(20)) + ) + end, + { + min_api_version = 14, + test_init = test_init_pan_tilt_only + } +) + test.register_coroutine_test( "PTZ Presets reports should generate appropriate events", function() @@ -2006,21 +2123,21 @@ test.register_coroutine_test( { capability = "mechanicalPanTiltZoom", component = "main", command = "panRelative", args = { 10 } }, }) test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, 10, 0, 0) + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, 10, nil, nil) }) test.socket.capability:__queue_receive({ mock_device.id, { capability = "mechanicalPanTiltZoom", component = "main", command = "tiltRelative", args = { -35 } }, }) test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, 0, -35, 0) + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, nil, -35, nil) }) test.socket.capability:__queue_receive({ mock_device.id, { capability = "mechanicalPanTiltZoom", component = "main", command = "zoomRelative", args = { 80 } }, }) test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, 0, 0, 80) + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZRelativeMove(mock_device, CAMERA_EP, nil, nil, 80) }) end, { @@ -2061,7 +2178,7 @@ test.register_coroutine_test( { capability = "mechanicalPanTiltZoom", component = "main", command = "setPan", args = { 50 } }, }) test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 50, 20, 30) + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 50, nil, nil) }) test.socket.matter:__queue_receive({ mock_device.id, @@ -2077,7 +2194,7 @@ test.register_coroutine_test( { capability = "mechanicalPanTiltZoom", component = "main", command = "setTilt", args = { -44 } }, }) test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 50, -44, 30) + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, nil, -44, nil) }) test.socket.matter:__queue_receive({ mock_device.id, @@ -2093,7 +2210,7 @@ test.register_coroutine_test( { capability = "mechanicalPanTiltZoom", component = "main", command = "setZoom", args = { 5 } }, }) test.socket.matter:__expect_send({ - mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, 50, -44, 5) + mock_device.id, clusters.CameraAvSettingsUserLevelManagement.server.commands.MPTZSetPosition(mock_device, CAMERA_EP, nil, nil, 5) }) test.socket.matter:__queue_receive({ mock_device.id, From 8f2511c494da96a929c7213a36fa2234eba41706 Mon Sep 17 00:00:00 2001 From: Harrison Carter <137556605+hcarter-775@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:04:44 -0500 Subject: [PATCH 096/102] Matter Sensor: Support MCD for FP400 and reprofiling (#3118) --- .../matter-sensor/profiles/aqara-fp400.yml | 56 +++++ .../sensor_handlers/attribute_handlers.lua | 4 +- .../sensor_utils/update_metadata_request.lua | 45 ++++ .../matter-sensor/src/sensor_utils/utils.lua | 44 ++++ .../device_configuration.lua | 44 ++-- .../air_quality_sensor_utils/utils.lua | 44 ---- .../sub_drivers/air_quality_sensor/init.lua | 3 +- .../src/sub_drivers/aqara_fp400/init.lua | 52 ++++- .../src/test/test_matter_aqara_fp400.lua | 192 ++++++++++++++++-- 9 files changed, 391 insertions(+), 93 deletions(-) create mode 100644 drivers/SmartThings/matter-sensor/src/sensor_utils/update_metadata_request.lua diff --git a/drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml b/drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml index c2d5b7b037..d4c9fde930 100644 --- a/drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml +++ b/drivers/SmartThings/matter-sensor/profiles/aqara-fp400.yml @@ -12,3 +12,59 @@ components: version: 1 categories: - name: PresenceSensor +- id: sensor1 + capabilities: + - id: presenceSensor + version: 1 + optional: true + categories: + - name: PresenceSensor +- id: sensor2 + capabilities: + - id: presenceSensor + version: 1 + optional: true + categories: + - name: PresenceSensor +- id: sensor3 + capabilities: + - id: presenceSensor + version: 1 + optional: true + categories: + - name: PresenceSensor +- id: sensor4 + capabilities: + - id: presenceSensor + version: 1 + optional: true + categories: + - name: PresenceSensor +- id: sensor5 + capabilities: + - id: presenceSensor + version: 1 + optional: true + categories: + - name: PresenceSensor +- id: sensor6 + capabilities: + - id: presenceSensor + version: 1 + optional: true + categories: + - name: PresenceSensor +- id: sensor7 + capabilities: + - id: presenceSensor + version: 1 + optional: true + categories: + - name: PresenceSensor +- id: sensor8 + capabilities: + - id: presenceSensor + version: 1 + optional: true + categories: + - name: PresenceSensor diff --git a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua index 1867d65e0b..514bb622cf 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_handlers/attribute_handlers.lua @@ -185,9 +185,9 @@ end function AttributeHandlers.occupancy_measured_value_handler(driver, device, ib, response) if device:supports_capability(capabilities.motionSensor) then - 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()) else - device:emit_event(ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present")) + device:emit_event_for_endpoint(ib.endpoint_id, ib.data.value == 0x01 and capabilities.presenceSensor.presence("present") or capabilities.presenceSensor.presence("not present")) end end diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/update_metadata_request.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/update_metadata_request.lua new file mode 100644 index 0000000000..abeb04e548 --- /dev/null +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/update_metadata_request.lua @@ -0,0 +1,45 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local UpdateMetadataRequest = {} +UpdateMetadataRequest.__index = UpdateMetadataRequest + +function UpdateMetadataRequest.init() + local update_metadata_request = setmetatable({}, UpdateMetadataRequest) + update_metadata_request.profile = nil + update_metadata_request.enabled_components = {} + return update_metadata_request +end + +function UpdateMetadataRequest:add_capabilities_to_component(component_id, capability_ids) + if not self.enabled_components[component_id] then + self.enabled_components[component_id] = {} + end + for _, capability_id in ipairs(capability_ids) do + table.insert(self.enabled_components[component_id], capability_id) + end + return self +end + +function UpdateMetadataRequest:add_profile(profile) + self.profile = profile + return self +end + +function UpdateMetadataRequest:formatted_enabled_components() + local formatted_enabled_components = {} + for component_id, capability_ids in pairs(self.enabled_components) do + table.insert(formatted_enabled_components, { component_id, capability_ids }) + end + return formatted_enabled_components +end + +function UpdateMetadataRequest:format_request() + local formatted_request = { + profile = self.profile, + optional_component_capabilities = self:formatted_enabled_components() + } + return formatted_request +end + +return UpdateMetadataRequest diff --git a/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua index 513c9f4602..75aa284aab 100644 --- a/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua +++ b/drivers/SmartThings/matter-sensor/src/sensor_utils/utils.lua @@ -36,4 +36,48 @@ function utils.tbl_contains(array, value) return false end +--- Deeply compare two values. +--- Handles metatables. Can optionally ignore cycle checking and/or function differences. +--- +--- @param a any +--- @param b any +--- @param opts table|nil { ignore_functions = boolean, ignore_cycles = boolean } +--- @param seen table|nil +--- @return boolean +function utils.deep_equals(a, b, opts, seen) + if a == b then return true end -- same object + if type(a) ~= type(b) then return false end -- different type + if type(a) == "function" and opts and opts.ignore_functions then return true end + if type(a) ~= "table" then return false end -- same type but not table, thus was already compared + + -- check for cycles in table references and preserve reference topology. + if not (opts and opts.ignore_cycles) then + seen = seen or {} + seen[a] = seen[a] or {} + if seen[a][b] then + return seen[a][b] + end + seen[a][b] = true + end + + -- Compare keys/values from a + for k, v in pairs(a) do + if not utils.deep_equals(v, b[k], opts, seen) then + return false + end + end + + -- Ensure b doesn't have extra keys + for k in pairs(b) do + if a[k] == nil then + return false + end + end + + -- Compare metatables + local mt_a = getmetatable(a) + local mt_b = getmetatable(b) + return utils.deep_equals(mt_a, mt_b, opts, seen) +end + return utils diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua index a30eaed0f8..9a8215feff 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/device_configuration.lua @@ -30,53 +30,37 @@ function DeviceConfiguration.supported_level_measurements(device) return measurement_caps, level_caps end --- Match Modular Profile function DeviceConfiguration.match_profile(device) + local update_metadata_request = require "sensor_utils.update_metadata_request" + local temp_eps = embedded_cluster_utils.get_endpoints(device, clusters.TemperatureMeasurement.ID) local humidity_eps = embedded_cluster_utils.get_endpoints(device, clusters.RelativeHumidityMeasurement.ID) - local optional_supported_component_capabilities = {} - local main_component_capabilities = {} - local profile_name - local MAIN_COMPONENT_IDX = 1 - local CAPABILITIES_LIST_IDX = 2 + local updated_metadata = update_metadata_request.init() + local preference_tags = "" if #temp_eps > 0 then - table.insert(main_component_capabilities, capabilities.temperatureMeasurement.ID) + updated_metadata:add_capabilities_to_component("main", { capabilities.temperatureMeasurement.ID }) + preference_tags = preference_tags .. "-temp" end if #humidity_eps > 0 then - table.insert(main_component_capabilities, capabilities.relativeHumidityMeasurement.ID) + updated_metadata:add_capabilities_to_component("main", { capabilities.relativeHumidityMeasurement.ID }) + preference_tags = preference_tags .. "-humidity" end local measurement_caps, level_caps = DeviceConfiguration.supported_level_measurements(device) + updated_metadata:add_capabilities_to_component("main", measurement_caps) + updated_metadata:add_capabilities_to_component("main", level_caps) - for _, cap_id in ipairs(measurement_caps) do - table.insert(main_component_capabilities, cap_id) - end - - for _, cap_id in ipairs(level_caps) do - table.insert(main_component_capabilities, cap_id) - end - - table.insert(optional_supported_component_capabilities, {"main", main_component_capabilities}) - - if #temp_eps > 0 and #humidity_eps > 0 then - profile_name = "aqs-modular-temp-humidity" - elseif #temp_eps > 0 then - profile_name = "aqs-modular-temp" - elseif #humidity_eps > 0 then - profile_name = "aqs-modular-humidity" - else - profile_name = "aqs-modular" - end - - device:try_update_metadata({profile = profile_name, optional_component_capabilities = optional_supported_component_capabilities}) + device:try_update_metadata(updated_metadata:add_profile("aqs-modular" .. preference_tags):format_request()) -- earlier modular profile gating (min api v14, rpc 8) ensures we are running >= 0.57 FW. -- This gating specifies a workaround required only for 0.57 FW, which is not needed for 0.58 and higher. if version.api < 15 or version.rpc < 9 then + local MAIN_COMPONENT_IDX = 1 + local CAPABILITIES_LIST_IDX = 2 -- add mandatory capabilities for subscription - local total_supported_capabilities = optional_supported_component_capabilities + local total_supported_capabilities = updated_metadata:formatted_enabled_components() table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.airQualityHealthConcern.ID) table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.refresh.ID) table.insert(total_supported_capabilities[MAIN_COMPONENT_IDX][CAPABILITIES_LIST_IDX], capabilities.firmwareUpdate.ID) diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua index 2e8d9e487b..a24149cce3 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/air_quality_sensor_utils/utils.lua @@ -90,48 +90,4 @@ function AirQualitySensorUtils.convert_value_to_unit(value, from_unit, to_unit, return conversion_function(value, fields.molecular_weights[capability_name]) end ---- Deeply compare two values. ---- Handles metatables. Can optionally ignore cycle checking and/or function differences. ---- ---- @param a any ---- @param b any ---- @param opts table|nil { ignore_functions = boolean, ignore_cycles = boolean } ---- @param seen table|nil ---- @return boolean -function AirQualitySensorUtils.deep_equals(a, b, opts, seen) - if a == b then return true end -- same object - if type(a) ~= type(b) then return false end -- different type - if type(a) == "function" and opts and opts.ignore_functions then return true end - if type(a) ~= "table" then return false end -- same type but not table, thus was already compared - - -- check for cycles in table references and preserve reference topology. - if not (opts and opts.ignore_cycles) then - seen = seen or {} - seen[a] = seen[a] or {} - if seen[a][b] then - return seen[a][b] - end - seen[a][b] = true - end - - -- Compare keys/values from a - for k, v in pairs(a) do - if not AirQualitySensorUtils.deep_equals(v, b[k], opts, seen) then - return false - end - end - - -- Ensure b doesn't have extra keys - for k in pairs(b) do - if a[k] == nil then - return false - end - end - - -- Compare metatables - local mt_a = getmetatable(a) - local mt_b = getmetatable(b) - return AirQualitySensorUtils.deep_equals(mt_a, mt_b, opts, seen) -end - return AirQualitySensorUtils diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua index 383af643a6..1ce1cc2176 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/air_quality_sensor/init.lua @@ -4,6 +4,7 @@ local version = require "version" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" +local sensor_utils = require "sensor_utils.utils" local aqs_utils = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.utils" local fields = require "sub_drivers.air_quality_sensor.air_quality_sensor_utils.fields" local attribute_handlers = require "sub_drivers.air_quality_sensor.air_quality_sensor_handlers.attribute_handlers" @@ -67,7 +68,7 @@ function AirQualitySensorLifecycleHandlers.device_init(driver, device) end function AirQualitySensorLifecycleHandlers.info_changed(driver, device, event, args) - if not aqs_utils.deep_equals(device.profile, args.old_st_store.profile, { ignore_functions = true }) then + if not sensor_utils.deep_equals(device.profile, args.old_st_store.profile, { ignore_functions = true }) then if device:get_field(fields.SUPPORTED_COMPONENT_CAPABILITIES) then --re-up subscription with new capabilities using the modular supports_capability override device:extend_device("supports_capability_by_id", aqs_utils.supports_capability_by_id_modular) diff --git a/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/init.lua b/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/init.lua index 5f47c86047..d9e9ad6378 100644 --- a/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/init.lua +++ b/drivers/SmartThings/matter-sensor/src/sub_drivers/aqara_fp400/init.lua @@ -1,21 +1,67 @@ -- Copyright 2026 SmartThings, Inc. -- Licensed under the Apache License, Version 2.0 +local capabilities = require "st.capabilities" +local sensor_utils = require "sensor_utils.utils" + +local COMPONENT_TO_ENDPOINT_MAP = "__COMPONENT_TO_ENDPOINT_MAP" + +local function endpoint_to_component(device, endpoint_id) + local map = device:get_field(COMPONENT_TO_ENDPOINT_MAP) or {} + for component_id, ep_id in pairs(map) do + if ep_id == endpoint_id then return component_id end + end + return "main" +end + +local function configure_profile(device) + local update_metadata_request = require "sensor_utils.update_metadata_request" + + local updated_component_to_endpoint_map = {} + local updated_metadata = update_metadata_request.init() + for _, ep in ipairs(device.endpoints) do + -- >2 since EP0 is the root node, EP1 is a permanent Presence Sensor, and EP2 is a permanent Light Sensor + if ep.endpoint_id > 2 then + local component_id = string.format("sensor%d", ep.endpoint_id - 2) -- ex. sensor1, sensor2, etc. for EP3, EP4, etc. + updated_component_to_endpoint_map[component_id] = ep.endpoint_id + updated_metadata:add_capabilities_to_component(component_id, { capabilities.presenceSensor.ID }) + end + end + device:set_field(COMPONENT_TO_ENDPOINT_MAP, updated_component_to_endpoint_map, { persist = true }) + device:try_update_metadata(updated_metadata:add_profile("aqara-fp400"):format_request()) +end + + local Fp400LifecycleHandlers = {} --- overwrite to avoid unnecessary metadata update calls -function Fp400LifecycleHandlers.do_configure() end +function Fp400LifecycleHandlers.do_configure(driver, device) + configure_profile(device) +end --- overwrite to avoid unnecessary metadata update calls function Fp400LifecycleHandlers.driver_switched(driver, device) + configure_profile(device) device:try_update_metadata({provisioning_state = "PROVISIONED"}) end +function Fp400LifecycleHandlers.info_changed(driver, device, event, args) + if not sensor_utils.deep_equals(args.old_st_store.endpoints, device.endpoints) then + configure_profile(device) + device:subscribe() + end +end + +function Fp400LifecycleHandlers.device_init(driver, device) + device:set_endpoint_to_component_fn(endpoint_to_component) + device:subscribe() +end + local aqara_fp400_handler = { NAME = "aqara-fp400", lifecycle_handlers = { doConfigure = Fp400LifecycleHandlers.do_configure, driverSwitched = Fp400LifecycleHandlers.driver_switched, + infoChanged = Fp400LifecycleHandlers.info_changed, + init = Fp400LifecycleHandlers.device_init, }, can_handle = require("sub_drivers.aqara_fp400.can_handle"), } diff --git a/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua b/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua index ba4f2fb296..6ce8abf91d 100644 --- a/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua +++ b/drivers/SmartThings/matter-sensor/src/test/test_matter_aqara_fp400.lua @@ -2,9 +2,11 @@ -- Licensed under the Apache License, Version 2.0 local test = require "integration_test" +local st_utils = require "st.utils" local capabilities = require "st.capabilities" local clusters = require "st.matter.clusters" local t_utils = require "integration_test.utils" +local sensor_utils = require "sensor_utils.utils" local matter_endpoints = { { @@ -20,16 +22,49 @@ local matter_endpoints = { endpoint_id = 1, clusters = { {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor + } + }, + { + endpoint_id = 2, + clusters = { {cluster_id = clusters.IlluminanceMeasurement.ID, cluster_type = "SERVER"}, }, + device_types = { + {device_type_id = 0x0106, device_type_revision = 1} -- Light Sensor + } + }, + { + endpoint_id = 3, + clusters = { + {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + }, device_types = { {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor } - } + }, + { + endpoint_id = 5, + clusters = { + {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor + } + }, +} + +local enabled_optional_component_capability_pairs = { + { "sensor1", { capabilities.presenceSensor.ID } }, + { "sensor3", { capabilities.presenceSensor.ID } } } local mock_device = test.mock_device.build_test_matter_device({ - profile = t_utils.get_profile_definition("aqara-fp400.yml"), + profile = t_utils.get_profile_definition("aqara-fp400.yml", + { enabled_optional_capabilities = enabled_optional_component_capability_pairs } + ), manufacturer_info = { vendor_id = 0x115F, product_id = 0x2009, @@ -37,31 +72,34 @@ local mock_device = test.mock_device.build_test_matter_device({ endpoints = matter_endpoints }) -local function subscribe_on_init(dev) +local function do_subscribe(dev) local subscribe_request = clusters.OccupancySensing.attributes.Occupancy:subscribe(dev) subscribe_request:merge(clusters.IlluminanceMeasurement.attributes.MeasuredValue:subscribe(dev)) return subscribe_request end local function test_init() - test.socket.matter:__set_channel_ordering("relaxed") - local subscribe_request = subscribe_on_init(mock_device) + local subscribe_request = do_subscribe(mock_device) test.socket.matter:__expect_send({mock_device.id, subscribe_request}) test.mock_device.add_test_device(mock_device) end test.set_test_init_function(test_init) test.register_coroutine_test( - "Test no profile change on doConfigure for FP400", + "Test profile change on doConfigure for FP400", function() test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) - -- The FP400 sub-driver overrides doConfigure to be a no-op -- When doConfigure completes successfully, the framework automatically provisions the device + mock_device:expect_metadata_update({ profile = "aqara-fp400", optional_component_capabilities = enabled_optional_component_capability_pairs }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) test.wait_for_events() + assert(sensor_utils.deep_equals(st_utils.deep_copy(mock_device:get_field("__COMPONENT_TO_ENDPOINT_MAP")), { + sensor1 = 3, + sensor3 = 5 + }), "Component to endpoint map should be set correctly on doConfigure") end, { - min_api_version = 14 + min_api_version = 21 } ) @@ -70,20 +108,21 @@ test.register_coroutine_test( function() local current_profile = mock_device.profile.id test.socket.device_lifecycle:__queue_receive({ mock_device.id, "driverSwitched" }) - -- The FP400 sub-driver overrides driverSwitched to only update provisioning state + mock_device:expect_metadata_update({ profile = "aqara-fp400", optional_component_capabilities = enabled_optional_component_capability_pairs }) mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) -- Ensure profile has not changed test.wait_for_events() assert(mock_device.profile.id == current_profile, "Profile should not change on driverSwitched") end, { - min_api_version = 14 + min_api_version = 21 } ) test.register_message_test( "Occupancy reports should generate correct presence messages", { + -- from EP1 { channel = "matter", direction = "receive", @@ -109,10 +148,73 @@ test.register_message_test( channel = "capability", direction = "send", message = mock_device:generate_test_message("main", capabilities.presenceSensor.presence("not present")) - } + }, + -- from EP3 + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 3, 1) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("sensor1", capabilities.presenceSensor.presence("present")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 3, 0) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("sensor1", capabilities.presenceSensor.presence("not present")) + }, + -- from EP5 + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 5, 1) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("sensor3", capabilities.presenceSensor.presence("present")) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_report_data(mock_device, 5, 0) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("sensor3", capabilities.presenceSensor.presence("not present")) + }, }, { - min_api_version = 14 + test_init = function () + local subscribe_request = do_subscribe(mock_device) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + test.mock_device.add_test_device(mock_device) + -- we can assume this will be set elsewhere-- see other tests. + mock_device:set_field("__COMPONENT_TO_ENDPOINT_MAP", + { sensor1 = 3, sensor3 = 5 }, { persist = true } + ) + end, + min_api_version = 14 } ) @@ -124,7 +226,7 @@ test.register_message_test( direction = "receive", message = { mock_device.id, - clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device, 1, 21370) + clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_report_data(mock_device, 2, 21370) } }, { @@ -138,4 +240,68 @@ test.register_message_test( } ) +test.register_coroutine_test( + "Test profile change on endpoints increment in infoChanged for FP400", + function() + local current_profile_id = mock_device.profile.id + local incremented_matter_endpoints = matter_endpoints + table.insert(incremented_matter_endpoints, { + endpoint_id = 4, + clusters = { + {cluster_id = clusters.OccupancySensing.ID, cluster_type = "SERVER"}, + }, + device_types = { + {device_type_id = 0x0107, device_type_revision = 1} -- Occupancy Sensor + } + }) + local incremented_enabled_optional_component_capability_pairs = { + { "sensor1", { capabilities.presenceSensor.ID } }, + { "sensor3", { capabilities.presenceSensor.ID } }, + { "sensor2", { capabilities.presenceSensor.ID } }, + } + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ endpoints = incremented_matter_endpoints })) + mock_device:expect_metadata_update({ profile = "aqara-fp400", optional_component_capabilities = incremented_enabled_optional_component_capability_pairs }) + local subscribe_request = do_subscribe(mock_device) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + -- Ensure profile has not changed + test.wait_for_events() + assert(mock_device.profile.id == current_profile_id, "Profile should not change on infoChanged") + assert(sensor_utils.deep_equals(st_utils.deep_copy(mock_device:get_field("__COMPONENT_TO_ENDPOINT_MAP")), { + sensor1 = 3, + sensor3 = 5, + sensor2 = 4 + }), "Component to endpoint map should be set correctly on doConfigure") + end, + { + min_api_version = 21 + } +) + +test.register_coroutine_test( + "Test profile change on endpoints decrement in infoChanged for FP400", + function() + local current_profile_id = mock_device.profile.id + local decremented_matter_endpoints = matter_endpoints + table.remove(decremented_matter_endpoints, 4) -- remove EP3 + local decremented_enabled_optional_component_capability_pairs = { + { "sensor3", { capabilities.presenceSensor.ID } }, + { "sensor2", { capabilities.presenceSensor.ID } }, + } + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ endpoints = decremented_matter_endpoints })) + mock_device:expect_metadata_update({ profile = "aqara-fp400", optional_component_capabilities = decremented_enabled_optional_component_capability_pairs }) + local subscribe_request = do_subscribe(mock_device) + test.socket.matter:__expect_send({mock_device.id, subscribe_request}) + -- Ensure profile has not changed + test.wait_for_events() + assert(mock_device.profile.id == current_profile_id, "Profile should not change on infoChanged") + assert(sensor_utils.deep_equals(st_utils.deep_copy(mock_device:get_field("__COMPONENT_TO_ENDPOINT_MAP")), { + sensor3 = 5, + sensor2 = 4 + }), "Component to endpoint map should be set correctly on doConfigure") + end, + { + min_api_version = 21 + } +) + test.run_registered_tests() From 4e89aa011bfbec8dce0d555a29b58a19b1fbd4ec Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 27 Aug 2026 11:40:36 -0500 Subject: [PATCH 097/102] WWSTCERT-13182 Govee Strip Light 2 (#3196) * WWSTCERT-13182 Govee Strip Light 2 * Update fingerprints.yml * Update fingerprints.yml --- .../matter-switch/fingerprints.yml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 29c85f6c79..1a56ecce38 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -809,7 +809,7 @@ matterManufacturer: productId: 0x6870 deviceProfileName: light-color-level - id: "4999/26272" - deviceLabel: Govee TV Backlight 3 Pro 55-65 + deviceLabel: Govee TV Backlight 3 PRO / 3S PRO vendorId: 0x1387 productId: 0x66A0 deviceProfileName: light-color-level @@ -898,6 +898,26 @@ matterManufacturer: vendorId: 0x1387 productId: 0x60B3 deviceProfileName: light-color-level + - id: "4999/6723" + deviceLabel: Govee Strip Light 2 + vendorId: 0x1387 + productId: 0x1A43 + deviceProfileName: light-color-level + - id: "4999/6001" + deviceLabel: Govee Wallwash Table Lamp + vendorId: 0x1387 + productId: 0x1771 + deviceProfileName: light-color-level + - id: "4999/6161" + deviceLabel: Govee Bubble Projector Light + vendorId: 0x1387 + productId: 0x1811 + deviceProfileName: light-color-level + - id: "4999/10816" + deviceLabel: Govee TV Backlight 3 + vendorId: 0x1387 + productId: 0x2A40 + deviceProfileName: light-color-level # Hager - id: "4741/8" deviceLabel: Hager matter 2 buttons (battery) From 3ff7c95fcdabbca1d2244e44125ed836bc42c368 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 27 Aug 2026 11:48:28 -0500 Subject: [PATCH 098/102] WWSTCERT-11168 Smart Radiator Thermostat X (#2949) --- .../matter-thermostat/fingerprints.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/drivers/SmartThings/matter-thermostat/fingerprints.yml b/drivers/SmartThings/matter-thermostat/fingerprints.yml index 814261790e..399f3861fc 100644 --- a/drivers/SmartThings/matter-thermostat/fingerprints.yml +++ b/drivers/SmartThings/matter-thermostat/fingerprints.yml @@ -89,6 +89,21 @@ matterManufacturer: vendorId: 0x134E productId: 0x0002 deviceProfileName: thermostat-humidity-heating-only-nostate-nobattery + - id: "4942/7" + deviceLabel: Smart Thermostat X (2nd Gen) + vendorId: 0x134E + productId: 0x0007 + deviceProfileName: thermostat-humidity-heating-only-nostate-batteryLevel + - id: "4942/8" + deviceLabel: Wireless Temperature Sensor X (2nd Gen) + vendorId: 0x134E + productId: 0x0008 + deviceProfileName: thermostat-humidity-heating-only-nostate-batteryLevel + - id: "4942/9" + deviceLabel: Smart Radiator Thermostat X + vendorId: 0x134E + productId: 0x0009 + deviceProfileName: thermostat-humidity-heating-only-nostate-batteryLevel #Taruie - id: "5151/4101" deviceLabel: TARUIE AC Remote From cfaf114c7219cc7b5574cc8fe0e0193e3abae5c1 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 27 Aug 2026 13:38:44 -0500 Subject: [PATCH 099/102] WWSTCERT-13051 Govee Monitor Light Bar (#3178) --- .../matter-switch/fingerprints.yml | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index 1a56ecce38..3609cb7910 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -898,6 +898,35 @@ matterManufacturer: vendorId: 0x1387 productId: 0x60B3 deviceProfileName: light-color-level + - id: "4999/10240" + deviceLabel: Govee Monitor Light Bar + vendorId: 0x1387 + productId: 0x2800 + deviceProfileName: light-color-level + - id: "4999/6833" + deviceLabel: Govee COB Strip Light 2 9.8ft/3m + vendorId: 0x1387 + productId: 0x1AB1 + deviceProfileName: light-color-level + - id: "4999/6834" + deviceLabel: Govee COB Strip Light 2 16.4ft/5m + vendorId: 0x1387 + productId: 0x1AB2 + deviceProfileName: light-color-level + - id: "4999/6835" + deviceLabel: Govee COB Strip Light 2 32.8ft/10m + vendorId: 0x1387 + productId: 0x1AB3 + deviceProfileName: light-color-level + - id: "4999/6818" + deviceLabel: Govee COB Strip Light 2 Pro 6.5ft/2m + vendorId: 0x1387 + productId: 0x1AA2 + deviceProfileName: light-color-level + - id: "4999/6821" + deviceLabel: Govee COB Strip Light 2 Pro 16.4ft/5m + vendorId: 0x1387 + productId: 0x1AA5 - id: "4999/6723" deviceLabel: Govee Strip Light 2 vendorId: 0x1387 From 7c957a9459ff32e6413fe03a5506132ddfe493df Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Thu, 27 Aug 2026 14:01:29 -0500 Subject: [PATCH 100/102] WWSTCERT-13508 Onvis Outlet T20 (#3199) --- 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 3609cb7910..b840abbd57 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -1688,6 +1688,11 @@ matterManufacturer: vendorId: 0x143D productId: 0x1001 deviceProfileName: plug-binary + - id: "5181/4098" + deviceLabel: Onvis Outlet T20 + vendorId: 0x143D + productId: 0x1002 + deviceProfileName: plug-power-energy-powerConsumption #Osram - id: "4489/2564" deviceLabel: OSRAM MATTER PLUG UK From b6d7875441aaedcfe89063ebcd434d36efc5c717 Mon Sep 17 00:00:00 2001 From: Chris Baumler Date: Mon, 31 Aug 2026 11:35:41 -0500 Subject: [PATCH 101/102] WWSTCERT-13411 Z-Wave Smart Plug (#3207) --- drivers/SmartThings/zwave-switch/fingerprints.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/SmartThings/zwave-switch/fingerprints.yml b/drivers/SmartThings/zwave-switch/fingerprints.yml index f606edf119..8719899d1d 100644 --- a/drivers/SmartThings/zwave-switch/fingerprints.yml +++ b/drivers/SmartThings/zwave-switch/fingerprints.yml @@ -885,6 +885,12 @@ zwaveManufacturer: productId: 0xFF97 productType: 0xFF01 deviceProfileName: switch-binary + - id: "0312/FF00/FF0C" + deviceLabel: Z-Wave Smart Plug + manufacturerId: 0x0312 + productId: 0xFF0C + productType: 0xFF00 + deviceProfileName: smartplug-binary - id: Evolve/RelaySwitch deviceLabel: Evolve Switch manufacturerId: 0x0113 From 650717a1e75506d5848386592a25308c3ba4f420 Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Fri, 28 Aug 2026 11:30:22 -0500 Subject: [PATCH 102/102] Fix profile name in fingerprint for Govee COB Strip Light (#3200) --- drivers/SmartThings/matter-switch/fingerprints.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/SmartThings/matter-switch/fingerprints.yml b/drivers/SmartThings/matter-switch/fingerprints.yml index b840abbd57..dbfe3156b7 100644 --- a/drivers/SmartThings/matter-switch/fingerprints.yml +++ b/drivers/SmartThings/matter-switch/fingerprints.yml @@ -927,6 +927,7 @@ matterManufacturer: deviceLabel: Govee COB Strip Light 2 Pro 16.4ft/5m vendorId: 0x1387 productId: 0x1AA5 + deviceProfileName: light-color-level - id: "4999/6723" deviceLabel: Govee Strip Light 2 vendorId: 0x1387